百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

《理解ASP.NET Core》系列7-文件服务器(File Server)

cac55 2024-10-01 07:44 21 浏览 0 评论

提供静态文件

静态文件默认存放在 Web根目录(Web Root) 中,路径为 项目根目录(Content Root) 下的wwwroot文件夹,也就是{Content Root}/wwwroot

如果你调用了Host.CreateDefaultBuilder方法,那么在该方法中,会通过UseContentRoot方法,将程序当前工作目录(Directory.GetCurrentDirectory())设置为项目根目录。具体可以查看主机一节。

csharp

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

当然,你也可以通过UseWebRoot扩展方法将默认的路径{Content Root}/wwwroot修改为自定义目录(不过,你改它干啥捏?)

csharp

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

            webBuilder.UseStartup<Startup>();
        });

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

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

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

接着,我们通过UseStaticFiles扩展方法,来注册静态文件中间件StaticFileMiddleware

csharp

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中间件进行一些额外的配置,了解一下配置项:

csharp

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 写入之前进行调用
    // 用于添加或更改 Headers
    public Action<StaticFileResponseContext> OnPrepareResponse { get; set; }
}

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

  • wwwroot
    • config.json
  • files
    • file.json

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

csharp

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,用于自定义目录视图。

csharp

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

示例如下:

csharp

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个文件名。

csharp

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

示例如下:

csharp

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集成了UseStaticFilesUseDefaultFilesUseDirectoryBrowser的功能,用起来方便一些,也是我们项目中使用的首选扩展方法。

先看一下FileServerOptions

csharp

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包含了StaticFileOptionsDirectoryBrowserOptionsDefaultFilesOptions三个选项,可以针对StaticFileMiddlewareDirectoryBrowserMiddlewareDefaultFilesMiddleware进行自定义配置。另外,其默认启用了静态文件和默认页,禁用了目录浏览。

下面举个例子熟悉一下:

假设文件目录:

  • files
    • images
      • 1.jpg
    • file.json
    • myindex.html

csharp

public void ConfigureServices(IServiceCollection services)
{
    // 如果将 EnableDirectoryBrowsing 设为 true,记得注册服务
    services.AddDirectoryBrowser();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{   
    // 启用 StaticFileMiddleware
    // 启用 DefaultFilesMiddleware
    // 禁用 DirectoryBrowserMiddleware
    // 默认指向 wwwroot
    app.UseFileServer();
    
    // 针对 files 文件夹配置
    var fileServerOptions = new FileServerOptions
    {
        FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),
        RequestPath = "/files",
        EnableDirectoryBrowsing = true
    };
    fileServerOptions.StaticFileOptions.OnPrepareResponse = ctx =>
    {
        // 配置缓存600s
        ctx.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接口:

csharp

public interface IFileProvider
{
    // 获取给定路径的目录信息,可枚举该目录中的所有文件
    IDirectoryContents GetDirectoryContents(string subpath);

    // 获取给定路径的文件信息
    IFileInfo GetFileInfo(string subpath);

    // 创建指定 filter 的 ChangeToken
    IChangeToken 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)长度
    // 如果是目录或文件不存在,则是 -1
    long Length { get; }

    // 目录或文件名,纯文件名,不包括路径
    string Name { get; }

    // 文件路径,包含文件名
    // 如果文件无法直接访问,则返回 null
    string 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 文件的示例:

xml

<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 目录下文件的访问:

csharp

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用于将多种文件提供程序进行集成。

如:

csharp

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

csharp

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中间件:

csharp

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

紧接着查看StaticFileMiddlewareInvoke方法:

csharp

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 中是否支持该 ContentType
        if (contentTypeProvider.TryGetContentType(subPath.Value, out contentType))
        {
            return true;
        }

        // 如果提供未知文件类型,则将其设置为默认 ContentType
        if (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中间件:

csharp

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

紧接着查看DirectoryBrowserMiddlewareInvoke方法:

csharp

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中间件:

csharp

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

紧接着查看DefaultFilesMiddlewareInvoke方法:

csharp

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并不是某个具体的中间件,它的实现还是依赖了StaticFileMiddlewareDirectoryBrowserMiddlewareDefaultFilesMiddleware这3个中间件。不过,我们可以看一下UseFileServer里的逻辑:

csharp

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中,包含ContentRootFileProviderWebRootFileProvider两个文件提供程序。下面我们就看一下他们是如何被初始化的。

csharp

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;
        // 初始化 ContentRootFileProvider
        hostingEnvironment.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);
            }
            
            // 初始化 WebRootFileProvider
            hostingEnvironment.WebRootFileProvider = new PhysicalFileProvider(hostingEnvironment.WebRootPath);
        }
        else
        {
            hostingEnvironment.WebRootFileProvider = new NullFileProvider();
        }

        hostingEnvironment.EnvironmentName =
            options.Environment ??
            hostingEnvironment.EnvironmentName;
    }
}

注意

  • 使用UseDirectoryBrowserUseStaticFiles提供文件浏览和访问时,URL 受大小写和基础文件系统字符的限制。例如,Windows 不区分大小写,但 macOS 和 Linux 区分大小写。
  • 如果使用 IIS 托管应用,那么 IIS 自带的静态文件处理器是不工作的,均是使用 ASP.NET Core Module 进行处理的,包括静态文件处理。

小结

  • 使用UseFileServer扩展方法提供文件浏览和访问,其集成了UseStaticFilesUseDirectoryBrowserUseDefaultFiles三个中间件的功能。UseStaticFiles:注册StaticFilesMiddleware,提供文件访问UseDirectoryBrowser:注册DirectoryBrowserMiddleware,提供文件目录浏览UseDefaultFiles:注册DefaultFilesMiddleware,当Url未指定访问的文件名时,提供默认页。
  • 文件提供程序均实现了接口IFileProvider,常用的文件提供程序有以下三种:PhysicalFileProvider:提供物理文件系统的访问ManifestEmbeddedFileProvider:提供嵌入在程序集中的文件的访问CompositeFileProvider:用于将多种文件提供程序进行集成。
  • 可通过IWebHostingEnvironment获取ContentRootFileProvider(默认目录为项目根目录)和WebRootFileProvider(默认目录为Web根目录)。
  • 提供静态文件
  • 提供Web根目录之外的文件
  • 提供目录浏览
  • 提供默认页
  • UseFileServer
  • 核心配置项
  • FileProvider
  • glob模式
  • PhysicalFileProvider
  • ManifestEmbeddedFileProvider
  • CompositeFileProvider
  • ContentTypeProvider
  • 核心中间件
  • StaticFileMiddleware
  • DirectoryBrowserMiddleware
  • DefaultFilesMiddleware
  • FileServer
  • FileProvider in IWebHostingEnvironment
  • 注意
  • 小结


欢迎点赞+转发+关注!大家的支持是我分享最大的动力!!!

相关推荐

用闲置电脑当软路由安装OpenWRT(小白教程)

话说软路由系统OpenWRT用起来真是香,里面的好多功能都是普通路由无法实现的,由于众所周知的原因,在这里就不细说,等安装完自己体验吧。今天就介绍用一台闲置的电脑(自带两个网口)充当软路由,安装Ope...

一招把废旧路由器改成交换机(用旧路由器做交换机)

家里面的路由器用个几年,就会WIFI变卡,新路由器买回来,旧路由器就没什么用了?我在这里教大家把老路由器变成交换机。近两年新出的路由器,基本都是2个LAN口,接网络设备还需要买交换机,淘汰下来的路由器...

如何将PC电脑变成web服务器:将内网主机映射到外网实现远程访问

我是艾西,今天跟大家分享内容还是比较多人问的一个问题:如何将PC电脑变成web服务器。内网主机作为web服务器,内容包括本地内网映射、多层内网映射解决方案、绕过电信80端口封锁、DDNS功能的实现(非...

电脑怎么改Wi-Fi密码(电脑怎么改wifi密码视频教程)

一.电脑打开“任意浏览器ie/google浏览器等”——>地址栏里输入管理ip地址然后按“回车键”打开该地址,如下图所示。二.输入正确的管理员密码——>点击“登录”即可(下图是PC版本的路...

旧路由器不要扔,可当电脑无线网卡使用,你还不知道吧!

家里有旧路由器,卖二手又不值钱,扔了又可惜。想不到路由器还有以下这些功能:扩大Wifi覆盖范围;充当电脑无线网卡;把这个技巧学起来,提升网络冲浪的幸福感!导航栏路由器恢复出厂设置(通用教程)有线桥接无...

硬件大师AIDA64 5.60.3716更新下载:“认准”Win10

著名硬件测试工具AIDA64更新至5.60.3716Beta版,本次更新修复了Win10Build版本号检测错误问题,识别更准确。另外还添加了对ITEIT8738F传感器、ASRock主板、NVI...

互联网病毒木马与盗版软件流量产业链(一)

A.相关地下产业链整体深度分析可能很多用户都有这样的经历,就是不管打开什么网站,甚至根本就没有打开浏览器,都会跳出来一堆的弹窗广告。那么,这个用户要么是中的病毒木马,或者是使用了盗版软件。不管是...

穿越火线tenparty.dat文件损坏怎么办?

很多玩家在玩火线的时候经常会因弹出错误代码,而被退出游戏。下面就教大家一些常见错误代码的解决方案。方法/步骤1SX提示码提示说明:您的电脑出现1,xxx,0(xxx代表任意数字)提示码,存在游...

办公小技巧015:如何关闭Windows Defender安全中心

WindowsDefenderWindowsDefender是Widows中自带杀毒软件,可以检测及清除潜藏在操作系统里的间谍软件及广告软件。为电脑提供最高强度的安全防护,也被誉为Windows的...

Win7/8.1/10团灭:微软发现严重漏洞

据外媒报道称,微软已经停止为Windows7发布新的安全更新了,理由是IE存在严重漏洞。存在严重漏洞的IE按照微软的说法,这个远程代码执行漏洞存在于IE浏览器处理脚本引擎对象的内存中。该漏洞可能以一...

WinCC flexible 2008 SP4 的安装步骤及系统要求

1、软件安装过程安装注意事项(必须严格遵守):软件仅支持以下操作系统(必须是微软原版的操作系统,Ghost版系统不支持,如番茄花园、雨林木风、电脑城装机版等):WinCCflexible2008...

Windows三方杀毒防护软件可能问题以及使用建议

在处理ECSWindows相关案例中,我们遇到很多奇怪的操作系统问题,例如软件安装失败,无法激活操作系统,无法访问本地磁盘,网络访问受到影响,系统蓝屏,系统Hang等,排查发现这与客户安装的各类杀...

杀毒软件被指泄露个人隐私(杀毒软件查出来一定是毒吗)

最近的多篇报道显示,你使用的杀毒软件在监视着你,而不仅仅是你计算机上的文件。2014年的一项研究使用虚拟机监视了杀毒软件产品向企业发送了什么信息。他们发现,所有测试的杀毒软件都给电脑分配了一个唯一的识...

开源杀毒软件ClamAV在推出约20年后终于到达1.0版本

ClamAV是一个开源的反病毒引擎,用于检测木马、病毒、恶意软件和其他恶意威胁。与商业Windows反恶意软件程序相比,它的检测水平相当低,但开发工作已经持续了几十年。该工具可用于所有平台,尽管它主要...

【Excel函数使用】时分秒时间怎么转换成秒?(二)

本节主要分享的函数是IFERROR和NUMBERVALUE上回我们用MID和FIND函数已经将数值提取出来,但是一些错误的返回值显示“#VALUE!”,此时我们需要检验错误返回值,并将错误值返回指定值...

取消回复欢迎 发表评论: