AI智能
改变未来

理解ASP.NET Core – 文件服务器(File Server)

注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录

提供静态文件

静态文件默认存放在 Web根目录(Web Root) 中,路径为 项目根目录(Content Root) 下的

wwwroot

文件夹,也就是

{Content Root}/wwwroot

如果你调用了

Host.CreateDefaultBuilder

方法,那么在该方法中,会通过

UseContentRoot

方法,将程序当前工作目录(

Directory.GetCurrentDirectory()

)设置为项目根目录。具体可以查看主机一节。

public static IHostBuilder CreateHostBuilder(string[] args) =>Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>{webBuilder.UseStartup<Startup>();});

当然,你也可以通过

UseWebRoot

扩展方法将默认的路径

{Content Root}/wwwroot

修改为自定义目录(不过,你改它干啥捏?)

public static IHostBuilder CreateHostBuilder(string[] args) =>Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>{// 配置静态资源的根目录为 mywwwroot, 默认为 wwwrootwebBuilder.UseWebRoot("mywwwroot");webBuilder.UseStartup<Startup>();});

为了方便,后面均使用 wwwroot 来表示Web根目录

首先,我们先在 wwwroot 文件夹下创建一个名为 config.json 的文件,内容随便填写

注意,确保 wwwroot 下的文件的属性为“如果较新则复制”或“始终复制”。

接着,我们通过

UseStaticFiles

扩展方法,来注册静态文件中间件

StaticFileMiddleware

public void Configure(IApplicationBuilder app, IWebHostEnvironment env){app.UseStaticFiles();}

现在,尝试一下通过 http://localhost:5000/config.json 来获取 wwwroot/config.json 的文件内容吧

如果你的项目中启用SwaggerUI,那么你会发现,即使你没有手动通过调用

UseStaticFiles()

添加中间件,你也可以访问 wwwroot 文件下的文件,这是因为 SwaggerUIMiddleware 中使用了 StaticFileMiddleware

提供Web根目录之外的文件

上面我们已经能够提供 wwwroot 文件夹内的静态文件了,那如果我们的文件不在 wwwroot 文件夹内,那如何提供呢?

很简单,我们可以针对

StaticFileMiddleware

中间件进行一些额外的配置,了解一下配置项:

public abstract class SharedOptionsBase{// 用于自定义静态文件的相对请求路径public PathString RequestPath { get; set; }// 文件提供程序public IFileProvider FileProvider { get; set; }// 是否补全路径末尾斜杠“/”,并重定向public bool RedirectToAppendTrailingSlash { get; set; }}public class StaticFileOptions : SharedOptionsBase{// ContentType提供程序public IContentTypeProvider ContentTypeProvider { get; set; }// 如果 ContentTypeProvider 无法识别文件类型,是否仍作为默认文件类型提供public bool ServeUnknownFileTypes { get; set; }// 当 ServeUnknownFileTypes = true 时,若出现无法识别的文件类型,则将该属性的值作为此文件的类型// 当 ServeUnknownFileTypes = true 时,必须赋值该属性,才会生效public string DefaultContentType { get; set; }// 当注册了HTTP响应压缩中间件时,是否对文件进行压缩public HttpsCompressionMode HttpsCompression { get; set; } = HttpsCompressionMode.Compress;// 在HTTP响应的 Status Code 和 Headers 设置完毕之后,Body 写入之前进行调用// 用于添加或更改 Headerspublic Action<StaticFileResponseContext> OnPrepareResponse { get; set; }}

假设我们现在有这样一个文件目录结构:

  • wwwrootconfig.json
  • files
      file.json

    然后,除了用于提供 wwwroot 静态文件的中间件外,我们还要注册一个用于提供 files 静态文件的中间件:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env){// 提供 wwwroot 静态文件app.UseStaticFiles();// 提供 files 静态文件app.UseStaticFiles(new StaticFileOptions{FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),// 指定文件的访问路径,允许与 FileProvider 中的文件夹不同名// 如果不指定,则可通过 http://localhost:5000/file.json 获取,// 如果指定,则需要通过 http://localhost:5000/files/file.json 获取RequestPath = "/files",OnPrepareResponse = ctx =>{// 配置前端缓存 600s(为了后续示例的良好运行,建议先不要配置该Header)ctx.Context.Response.Headers.Add(HeaderNames.CacheControl, "public,max-age=600");}});}

    建议将公开访问的文件放置到 wwwroot 目录下,而将需要授权访问的文件放置到其他目录下(在调用

    UseAuthorization

    之后调用

    UseStaticFiles

    并指定文件目录)

    提供目录浏览

    上面,我们可以通过Url访问某一个文件的内容,而通过

    UseDirectoryBrowser

    ,注册

    DirectoryBrowserMiddleware

    中间件,可以让我们在浏览器中以目录的形式来访问文件列表。

    另外,

    DirectoryBrowserMiddleware

    中间件的可配置项除了

    SharedOptionsBase

    中的之外,还有一个

    Formatter

    ,用于自定义目录视图。

    public class DirectoryBrowserOptions : SharedOptionsBase{public IDirectoryFormatter Formatter { get; set; }}

    示例如下:

    public void ConfigureServices(IServiceCollection services){services.AddDirectoryBrowser();}public void Configure(IApplicationBuilder app, IWebHostEnvironment env){// 通过 http://localhost:5000,即可访问 wwwroot 目录app.UseDirectoryBrowser();// 通过 http://localhost:5000/files,即可访问 files 目录app.UseDirectoryBrowser(new DirectoryBrowserOptions{// 如果指定了没有在 UseStaticFiles 中提供的文件目录,虽然可以浏览文件列表,但是无法访问文件内容FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),// 这里一定要和 StaticFileOptions 中的 RequestPath 一致,否则会无法访问文件RequestPath = "/files"});}

    提供默认页

    通过

    UseDefaultFiles

    ,注册

    DefaultFilesMiddleware

    中间件,允许在访问静态文件、但未提供文件名的情况下(即传入的是一个目录的路径),提供默认页的展示。

    注意:

    UseDefaultFiles

    必须在

    UseStaticFiles

    之前进行调用。因为

    DefaultFilesMiddleware

    仅仅负责重写Url,实际上默认页文件,仍然是通过

    StaticFilesMiddleware

    来提供的。

    默认情况下,该中间件会按照顺序搜索文件目录下的HTML页面文件:

    • default.htm
    • default.html
    • index.htm
    • index.html

    另外,

    DefaultFilesMiddleware

    中间件的可配置项除了

    SharedOptionsBase

    中的之外,还有一个

    DefaultFileNames

    ,是个列表,用于自定义默认页的文件名,里面的默认值就是上面提到的4个文件名。

    public class DefaultFilesOptions : SharedOptionsBase{public IList<string> DefaultFileNames { get; set; }}

    示例如下:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env){// 会去 wwwroot 寻找 default.htm 、default.html 、index.htm 或 index.html 文件作为默认页app.UseDefaultFiles();// 设置 files 目录的默认页var defaultFilesOptions = new DefaultFilesOptions();defaultFilesOptions.DefaultFileNames.Clear();// 指定默认页名称defaultFilesOptions.DefaultFileNames.Add("index1.html");// 指定请求路径defaultFilesOptions.RequestPath = "/files";// 指定默认页所在的目录defaultFilesOptions.FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files"));app.UseDefaultFiles(defaultFilesOptions);}

    UseFileServer

    UseFileServer

    集成了

    UseStaticFiles

    UseDefaultFiles

    UseDirectoryBrowser

    的功能,用起来方便一些,也是我们项目中使用的首选扩展方法。

    先看一下

    FileServerOptions

    public class FileServerOptions : SharedOptionsBase{public FileServerOptions(): base(new SharedOptions()){StaticFileOptions = new StaticFileOptions(SharedOptions);DirectoryBrowserOptions = new DirectoryBrowserOptions(SharedOptions);DefaultFilesOptions = new DefaultFilesOptions(SharedOptions);EnableDefaultFiles = true;}public StaticFileOptions StaticFileOptions { get; private set; }public DirectoryBrowserOptions DirectoryBrowserOptions { get; private set; }public DefaultFilesOptions DefaultFilesOptions { get; private set; }// 默认禁用目录浏览public bool EnableDirectoryBrowsing { get; set; }// 默认启用默认页(在构造函数中初始化的)public bool EnableDefaultFiles { get; set; }}

    可以看到,

    FileServerOptions

    包含了

    StaticFileOptions

    DirectoryBrowserOptions

    DefaultFilesOptions

    三个选项,可以针对

    StaticFileMiddleware

    DirectoryBrowserMiddleware

    DefaultFilesMiddleware

    进行自定义配置。另外,其默认启用了静态文件和默认页,禁用了目录浏览。

    下面举个例子熟悉一下:

    假设文件目录:

    • filesimages1.jpg
  • file.json
  • myindex.html
  • public void ConfigureServices(IServiceCollection services){// 如果将 EnableDirectoryBrowsing 设为 true,记得注册服务services.AddDirectoryBrowser();}public void Configure(IApplicationBuilder app, IWebHostEnvironment env){// 启用 StaticFileMiddleware// 启用 DefaultFilesMiddleware// 禁用 DirectoryBrowserMiddleware// 默认指向 wwwrootapp.UseFileServer();// 针对 files 文件夹配置var fileServerOptions = new FileServerOptions{FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),RequestPath = "/files",EnableDirectoryBrowsing = true};fileServerOptions.StaticFileOptions.OnPrepareResponse = ctx =>{// 配置缓存600sctx.Context.Response.Headers.Add(HeaderNames.CacheControl, "public,max-age=600");};fileServerOptions.DefaultFilesOptions.DefaultFileNames.Clear();fileServerOptions.DefaultFilesOptions.DefaultFileNames.Add("myindex.html");app.UseFileServer(fileServerOptions);}

    当访问 http://localhost:5000/files 时,由于在

    DefaultFilesOptions.DefaultFileNames

    中添加了文件名

    myindex.html

    ,所以可以找到默认页,此时会显示默认页的内容。

    假如我们没有在

    DefaultFilesOptions.DefaultFileNames

    中添加文件名

    myindex.html

    ,那么便找不到默认页,但由于启用了

    DirectoryBrowsing

    ,所以此时会展示文件列表。

    核心配置项

    FileProvider

    上面我们已经见过

    PhysicalFileProvider

    了,它仅仅是众多文件提供程序中的一种。所有的文件提供程序均实现了

    IFileProvider

    接口:

    public interface IFileProvider{// 获取给定路径的目录信息,可枚举该目录中的所有文件IDirectoryContents GetDirectoryContents(string subpath);// 获取给定路径的文件信息IFileInfo GetFileInfo(string subpath);// 创建指定 filter 的 ChangeTokenIChangeToken Watch(string filter);}public interface IDirectoryContents : IEnumerable<IFileInfo>, IEnumerable{bool Exists { get; }}public interface IFileInfo{bool Exists { get; }bool IsDirectory { get; }DateTimeOffset LastModified { get; }// 字节(bytes)长度// 如果是目录或文件不存在,则是 -1long Length { get; }// 目录或文件名,纯文件名,不包括路径string Name { get; }// 文件路径,包含文件名// 如果文件无法直接访问,则返回 nullstring PhysicalPath { get; }// 创建该文件只读流Stream CreateReadStream();}

    常用的文件提供程序有以下三种:

    • PhysicalFileProvider
    • ManifestEmbeddedFileProvider
    • CompositeFileProvider

    glob模式

    在介绍这三种文件提供程序之前,先说一下

    glob模式

    ,即

    通配符模式

    。两个通配符分别是

    *

    **

    • *

      :匹配当前目录层级(不包含子目录)下的任何内容、任何文件名或任何文件扩展名,可以通过

      /

      \\

      .

      进行分隔。

    • **

      :匹配目录多层级(包含子目录)的任何内容,用于递归匹配多层级目录的多个文件。

    PhysicalFileProvider

    PhysicalFileProvider

    用于提供物理文件系统的访问。该提供程序需要将文件路径范围限定在一个目录及其子目录中,不能访问目录外部的内容。

    当实例化该文件提供程序时,需要提供一个绝对的目录路径,作为文件目录的root。

    PhysicalFileProvider

    目录或文件路径不支持glob(通配符)模式。

    ManifestEmbeddedFileProvider

    ManifestEmbeddedFileProvider

    用于提供嵌入在程序集中的文件的访问。

    可能你对这个嵌入文件比较陌生,没关系,请按照下面的步骤来:

    • 安装Nuget包:
      Install-Package Microsoft.Extensions.FileProviders.Embedded
    • 编辑
      .csproj

      文件:添加

      <GenerateEmbeddedFilesManifest>

      ,并设置为

      true
    • 使用
      <EmbeddedResource>

      添加要嵌入的文件

    以下是 .csproj 文件的示例:

    <Project Sdk="Microsoft.NET.Sdk.Web"><PropertyGroup><TargetFramework>net5.0</TargetFramework><GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest></PropertyGroup><ItemGroup><PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="5.0.11" /></ItemGroup><ItemGroup><EmbeddedResource Include="files\\**" /></ItemGroup></Project>

    现在我们通过

    ManifestEmbeddedFileProvider

    来提供嵌入到程序集的 files 目录下文件的访问:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env){var fileServerOptions = new FileServerOptions();fileServerOptions.StaticFileOptions.FileProvider = new ManifestEmbeddedFileProvider(Assembly.GetExecutingAssembly(), "/files");fileServerOptions.StaticFileOptions.RequestPath = "/files";app.UseFileServer(fileServerOptions);}

    现在,你可以通过 http://localhost:5000/files/file.json 来访问文件了。

    CompositeFileProvider

    CompositeFileProvider

    用于将多种文件提供程序进行集成。

    如:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env){var fileServerOptions = new FileServerOptions();var fileProvider = new CompositeFileProvider(env.WebRootFileProvider,new ManifestEmbeddedFileProvider(Assembly.GetExecutingAssembly(), "/files"));fileServerOptions.StaticFileOptions.FileProvider = fileProvider;fileServerOptions.StaticFileOptions.RequestPath = "/composite";app.UseFileServer(fileServerOptions);}

    现在,你可以通过 http://localhost:5000/composite/file.json 来访问文件了。

    ContentTypeProvider

    Http请求头中的

    Content-Type

    大家一定很熟悉,

    ContentTypeProvider

    就是用来提供文件扩展名和MIME类型映射关系的。

    若我们没有显示指定

    ContentTypeProvider

    ,则框架默认使用

    FileExtensionContentTypeProvider

    ,其实现了接口

    IContentTypeProvider

    public interface IContentTypeProvider{// 尝试根据文件路径,获取对应的 MIME 类型bool TryGetContentType(string subpath, out string contentType);}public class FileExtensionContentTypeProvider : IContentTypeProvider{public FileExtensionContentTypeProvider(): this(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase){// ...此处省略一万字}{}public FileExtensionContentTypeProvider(IDictionary<string, string> mapping){Mappings = mapping;}public IDictionary<string, string> Mappings { get; private set; }public bool TryGetContentType(string subpath, out string contentType){string extension = GetExtension(subpath);if (extension == null){contentType = null;return false;}return Mappings.TryGetValue(extension, out contentType);}private static string GetExtension(string path){// 没有使用 Path.GetExtension() 的原因是:当路径中存在无效字符时,其会抛出异常,而这里不应抛出异常。if (string.IsNullOrWhiteSpace(path)){return null;}int index = path.LastIndexOf(\'.\');if (index < 0){return null;}return path.Substring(index);}}

    FileExtensionContentTypeProvider

    的无参构造函数中,默认添加了380种已知的文件扩展名和MIME类型的映射,存放在

    Mappings

    属性中。你也可以添加自定义的映射,或移除不想要的映射。

    核心中间件

    StaticFileMiddleware

    通过

    UseStaticFiles

    扩展方法,可以方便的注册

    StaticFileMiddleware

    中间件:

    public static class StaticFileExtensions{public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app){return app.UseMiddleware<StaticFileMiddleware>();}public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, string requestPath){return app.UseStaticFiles(new StaticFileOptions{RequestPath = new PathString(requestPath)});}public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, StaticFileOptions options){return app.UseMiddleware<StaticFileMiddleware>(Options.Create(options));}}

    紧接着查看

    StaticFileMiddleware

    Invoke

    方法:

    public class StaticFileMiddleware{private readonly StaticFileOptions _options;private readonly PathString _matchUrl;private readonly RequestDelegate _next;private readonly ILogger _logger;private readonly IFileProvider _fileProvider;private readonly IContentTypeProvider _contentTypeProvider;public StaticFileMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<StaticFileOptions> options, ILoggerFactory loggerFactory){_next = next;_options = options.Value;// 若未指定 ContentTypeProvider,则默认使用 FileExtensionContentTypeProvider_contentTypeProvider = _options.ContentTypeProvider ?? new FileExtensionContentTypeProvider();// 若未指定 FileProvider,则默认使用 hostingEnv.WebRootFileProvider_fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);_matchUrl = _options.RequestPath;_logger = loggerFactory.CreateLogger<StaticFileMiddleware>();}public Task Invoke(HttpContext context){// 若已匹配到 Endpoint,则跳过if (!ValidateNoEndpoint(context)){_logger.EndpointMatched();}// 若HTTP请求方法不是 Get,也不是 Head,则跳过else if (!ValidateMethod(context)){_logger.RequestMethodNotSupported(context.Request.Method);}// 如果请求路径不匹配,则跳过else if (!ValidatePath(context, _matchUrl, out var subPath)){_logger.PathMismatch(subPath);}// 如果 ContentType 不受支持,则跳过else if (!LookupContentType(_contentTypeProvider, _options, subPath, out var contentType)){_logger.FileTypeNotSupported(subPath);}else{// 尝试提供静态文件return TryServeStaticFile(context, contentType, subPath);}return _next(context);}private static bool ValidateNoEndpoint(HttpContext context) => context.GetEndpoint() == null;private static bool ValidateMethod(HttpContext context) => Helpers.IsGetOrHeadMethod(context.Request.Method);internal static bool ValidatePath(HttpContext context, PathString matchUrl, out PathString subPath) => Helpers.TryMatchPath(context, matchUrl, forDirectory: false, out subPath);internal static bool LookupContentType(IContentTypeProvider contentTypeProvider, StaticFileOptions options, PathString subPath, out string contentType){// 查看 Provider 中是否支持该 ContentTypeif (contentTypeProvider.TryGetContentType(subPath.Value, out contentType)){return true;}// 如果提供未知文件类型,则将其设置为默认 ContentTypeif (options.ServeUnknownFileTypes){contentType = options.DefaultContentType;return true;}return false;}private Task TryServeStaticFile(HttpContext context, string contentType, PathString subPath){var fileContext = new StaticFileContext(context, _options, _logger, _fileProvider, contentType, subPath);// 如果文件不存在,则跳过if (!fileContext.LookupFileInfo()){_logger.FileNotFound(fileContext.SubPath);}else{// 若文件存在,则提供该静态文件return fileContext.ServeStaticFile(context, _next);}return _next(context);}}

    DirectoryBrowserMiddleware

    通过

    UseDirectoryBrowser

    扩展方法,可以方便的注册

    DirectoryBrowserMiddleware

    中间件:

    public static class DirectoryBrowserExtensions{public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app){return app.UseMiddleware<DirectoryBrowserMiddleware>();}public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, string requestPath){return app.UseDirectoryBrowser(new DirectoryBrowserOptions{RequestPath = new PathString(requestPath)});}public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, DirectoryBrowserOptions options){return app.UseMiddleware<DirectoryBrowserMiddleware>(Options.Create(options));}}

    紧接着查看

    DirectoryBrowserMiddleware

    Invoke

    方法:

    public class DirectoryBrowserMiddleware{private readonly DirectoryBrowserOptions _options;private readonly PathString _matchUrl;private readonly RequestDelegate _next;private readonly IDirectoryFormatter _formatter;private readonly IFileProvider _fileProvider;public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DirectoryBrowserOptions> options): this(next, hostingEnv, HtmlEncoder.Default, options){}public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, HtmlEncoder encoder, IOptions<DirectoryBrowserOptions> options){_next = next;_options = options.Value;// 若未指定 FileProvider,则默认使用 hostingEnv.WebRootFileProvider_fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);_formatter = _options.Formatter ?? new HtmlDirectoryFormatter(encoder);_matchUrl = _options.RequestPath;}public Task Invoke(HttpContext context){// 若已匹配到 Endpoint,则跳过// 若HTTP请求方法不是 Get,也不是 Head,则跳过// 如果请求路径不匹配,则跳过// 若文件目录不存在,则跳过if (context.GetEndpoint() == null&& Helpers.IsGetOrHeadMethod(context.Request.Method)&& Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath)&& TryGetDirectoryInfo(subpath, out var contents)){if (_options.RedirectToAppendTrailingSlash && !Helpers.PathEndsInSlash(context.Request.Path)){Helpers.RedirectToPathWithSlash(context);return Task.CompletedTask;}// 生成文件浏览视图return _formatter.GenerateContentAsync(context, contents);}return _next(context);}private bool TryGetDirectoryInfo(PathString subpath, out IDirectoryContents contents){contents = _fileProvider.GetDirectoryContents(subpath.Value);return contents.Exists;}}

    DefaultFilesMiddleware

    通过

    UseDefaultFiles

    扩展方法,可以方便的注册

    DefaultFilesMiddleware

    中间件:

    public static class DefaultFilesExtensions{public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app){return app.UseMiddleware<DefaultFilesMiddleware>();}public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, string requestPath){return app.UseDefaultFiles(new DefaultFilesOptions{RequestPath = new PathString(requestPath)});}public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, DefaultFilesOptions options){return app.UseMiddleware<DefaultFilesMiddleware>(Options.Create(options));}}

    紧接着查看

    DefaultFilesMiddleware

    Invoke

    方法:

    public class DefaultFilesMiddleware{private readonly DefaultFilesOptions _options;private readonly PathString _matchUrl;private readonly RequestDelegate _next;private readonly IFileProvider _fileProvider;public DefaultFilesMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DefaultFilesOptions> options){_next = next;_options = options.Value;// 若未指定 FileProvider,则默认使用 hostingEnv.WebRootFileProvider_fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);_matchUrl = _options.RequestPath;}public Task Invoke(HttpContext context){// 若已匹配到 Endpoint,则跳过// 若HTTP请求方法不是 Get,也不是 Head,则跳过// 如果请求路径不匹配,则跳过if (context.GetEndpoint() == null&& Helpers.IsGetOrHeadMethod(context.Request.Method)&& Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath)){var dirContents = _fileProvider.GetDirectoryContents(subpath.Value);if (dirContents.Exists){for (int matchIndex = 0; matchIndex < _options.DefaultFileNames.Count; matchIndex++){string defaultFile = _options.DefaultFileNames[matchIndex];var file = _fileProvider.GetFileInfo(subpath.Value + defaultFile);// 找到了默认页if (file.Exists){if (_options.RedirectToAppendTrailingSlash && !Helpers.PathEndsInSlash(context.Request.Path)){Helpers.RedirectToPathWithSlash(context);return Task.CompletedTask;}// 重写为默认页的Url,后续通过 StaticFileMiddleware 提供该页面context.Request.Path = new PathString(Helpers.GetPathValueWithSlash(context.Request.Path) + defaultFile);break;}}}}return _next(context);}}

    FileServer

    FileServer并不是某个具体的中间件,它的实现还是依赖了

    StaticFileMiddleware

    DirectoryBrowserMiddleware

    DefaultFilesMiddleware

    这3个中间件。不过,我们可以看一下

    UseFileServer

    里的逻辑:

    public static class FileServerExtensions{public static IApplicationBuilder UseFileServer(this IApplicationBuilder app){return app.UseFileServer(new FileServerOptions());}public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, bool enableDirectoryBrowsing){return app.UseFileServer(new FileServerOptions{EnableDirectoryBrowsing = enableDirectoryBrowsing});}public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, string requestPath){return app.UseFileServer(new FileServerOptions{RequestPath = new PathString(requestPath)});}public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, FileServerOptions options){// 启用默认页if (options.EnableDefaultFiles){app.UseDefaultFiles(options.DefaultFilesOptions);}// 启用目录浏览if (options.EnableDirectoryBrowsing){app.UseDirectoryBrowser(options.DirectoryBrowserOptions);}return app.UseStaticFiles(options.StaticFileOptions);}}

    FileProvider in IWebHostingEnvironment

    在接口

    IHostingEnvironment

    中,包含

    ContentRootFileProvider

    WebRootFileProvider

    两个文件提供程序。下面我们就看一下他们是如何被初始化的。

    internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider{private WebHostBuilderContext GetWebHostBuilderContext(HostBuilderContext context){if (!context.Properties.TryGetValue(typeof(WebHostBuilderContext), out var contextVal)){var options = new WebHostOptions(context.Configuration, Assembly.GetEntryAssembly()?.GetName().Name);var webHostBuilderContext = new WebHostBuilderContext{Configuration = context.Configuration,HostingEnvironment = new HostingEnvironment(),};// 重点在这里,看这个 Initialize 方法webHostBuilderContext.HostingEnvironment.Initialize(context.HostingEnvironment.ContentRootPath, options);context.Properties[typeof(WebHostBuilderContext)] = webHostBuilderContext;context.Properties[typeof(WebHostOptions)] = options;return webHostBuilderContext;}var webHostContext = (WebHostBuilderContext)contextVal;webHostContext.Configuration = context.Configuration;return webHostContext;}}internal static class HostingEnvironmentExtensions{internal static void Initialize(this IWebHostEnvironment hostingEnvironment, string contentRootPath, WebHostOptions options){hostingEnvironment.ApplicationName = options.ApplicationName;hostingEnvironment.ContentRootPath = contentRootPath;// 初始化 ContentRootFileProviderhostingEnvironment.ContentRootFileProvider = new PhysicalFileProvider(hostingEnvironment.ContentRootPath);var webRoot = options.WebRoot;if (webRoot == null){// 如果 /wwwroot 目录存在,则设置为Web根目录var wwwroot = Path.Combine(hostingEnvironment.ContentRootPath, "wwwroot");if (Directory.Exists(wwwroot)){hostingEnvironment.WebRootPath = wwwroot;}}else{hostingEnvironment.WebRootPath = Path.Combine(hostingEnvironment.ContentRootPath, webRoot);}if (!string.IsNullOrEmpty(hostingEnvironment.WebRootPath)){hostingEnvironment.WebRootPath = Path.GetFullPath(hostingEnvironment.WebRootPath);if (!Directory.Exists(hostingEnvironment.WebRootPath)){Directory.CreateDirectory(hostingEnvironment.WebRootPath);}// 初始化 WebRootFileProviderhostingEnvironment.WebRootFileProvider = new PhysicalFileProvider(hostingEnvironment.WebRootPath);}else{hostingEnvironment.WebRootFileProvider = new NullFileProvider();}hostingEnvironment.EnvironmentName =options.Environment ??hostingEnvironment.EnvironmentName;}}

    注意

    • 使用
      UseDirectoryBrowser

      UseStaticFiles

      提供文件浏览和访问时,URL 受大小写和基础文件系统字符的限制。例如,Windows 不区分大小写,但 macOS 和 Linux 区分大小写。

    • 如果使用 IIS 托管应用,那么 IIS 自带的静态文件处理器是不工作的,均是使用 ASP.NET Core Module 进行处理的,包括静态文件处理。

    小结

    • 使用
      UseFileServer

      扩展方法提供文件浏览和访问,其集成了

      UseStaticFiles

      UseDirectoryBrowser

      UseDefaultFiles

      三个中间件的功能。

      UseStaticFiles

      :注册

      StaticFilesMiddleware

      ,提供文件访问

    • UseDirectoryBrowser

      :注册

      DirectoryBrowserMiddleware

      ,提供文件目录浏览

    • UseDefaultFiles

      :注册

      DefaultFilesMiddleware

      ,当Url未指定访问的文件名时,提供默认页。

  • 文件提供程序均实现了接口
    IFileProvider

    ,常用的文件提供程序有以下三种:

      PhysicalFileProvider

      :提供物理文件系统的访问

    • ManifestEmbeddedFileProvider

      :提供嵌入在程序集中的文件的访问

    • CompositeFileProvider

      :用于将多种文件提供程序进行集成。

  • 可通过
    IWebHostingEnvironment

    获取

    ContentRootFileProvider

    (默认目录为项目根目录)和

    WebRootFileProvider

    (默认目录为Web根目录)。

  • 赞(0) 打赏
    未经允许不得转载:爱站程序员基地 » 理解ASP.NET Core – 文件服务器(File Server)