理解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, 默認爲 wwwroot
            webBuilder.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 寫入之前進行調用
    // 用於添加或更改 Headers
    public Action<StaticFileResponseContext> OnPrepareResponse { get; set; }
}

假設我們現在有這樣一個文件目錄結構:

  • wwwroot
    • config.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集成了UseStaticFilesUseDefaultFilesUseDirectoryBrowser的功能,用起來方便一些,也是我們項目中使用的首選擴展方法。

先看一下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包含了StaticFileOptionsDirectoryBrowserOptionsDefaultFilesOptions三個選項,可以針對StaticFileMiddlewareDirectoryBrowserMiddlewareDefaultFilesMiddleware進行自定義配置。另外,其默認啓用了靜態文件和默認頁,禁用了目錄瀏覽。

下面舉個例子熟悉一下:

假設文件目錄:

  • files
    • images
      • 1.jpg
    • file.json
    • myindex.html
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接口:

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 文件的示例:

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

緊接着查看StaticFileMiddlewareInvoke方法:

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

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方法:

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

緊接着查看DefaultFilesMiddlewareInvoke方法:

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裏的邏輯:

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兩個文件提供程序。下面我們就看一下他們是如何被初始化的。

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根目錄)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章