NetCore 入门 (二) : 文件系统

1. Quick Start

ASP.NET Core应用具有很多读取文件的场景,如读取配置文件、静态Web资源文件(js/css/image)、MVC应用的View文件、以及直接编译到程序集中的内嵌资源文件。这些文件的读取都需要一个IFileProvider对象。

IFileProvider对象构建了一个抽象的文件系统,不仅提供了统一的API来读取各种类型的文件,还能及时监控目标文件的变化。

1.1 安装NuGet包

Microsoft.Extensions.FileProviders.Abstractions // 抽象依赖包
Microsoft.Extensions.FileProviders.Embedded // 嵌入式文件系统
Microsoft.Extensions.FileProviders.Physical // 物理文件系统
Microsoft.Extensions.FileProviders.Composite // 复合型文件系统

1.2 示例1 - 遍历目录

现有文件目录如下所示:

F:/root
├─ dir1/
│  ├─ footbar/
│  │  ├─ bar.txt
│  │  └─ foo.txt
│  └─ baz.txt
├─dir2/
│  └─ quz.txt
├─ bzx.txt
└─ coding.txt

现在以F:/root为根目录,构建物理文件系统。

using (var fileProvider = new PhysicalFileProvider(@"F:\root"))
{
    Console.WriteLine("获取根目录");
    IDirectoryContents directoryContents = fileProvider.GetDirectoryContents("/");// 获取根目录

    foreach (var item in directoryContents)
    {
        Console.WriteLine($"Name:{item.Name},   IsDirectory:{item.IsDirectory},   {item.PhysicalPath}");
    }

    Console.WriteLine("获取目录dir1");
    directoryContents = fileProvider.GetDirectoryContents("/dir1");// 获取目录dir1

    foreach (var item in directoryContents)
    {
        Console.WriteLine($"Name:{item.Name},   IsDirectory:{item.IsDirectory},   {item.PhysicalPath}");
    }
}

输出结果如下:

获取根目录
Name:bzx.txt,   IsDirectory:False,   F:\root\bzx.txt
Name:coding.txt,   IsDirectory:False,   F:\root\coding.txt
Name:dir1,   IsDirectory:True,   F:\root\dir1
Name:dir2,   IsDirectory:True,   F:\root\dir2
获取目录dir1
Name:baz.txt,   IsDirectory:False,   F:\root\dir1\baz.txt
Name:footbar,   IsDirectory:True,   F:\root\dir1\footbar

1.3 示例2 - 读取文件内容

using (var fileProvider = new PhysicalFileProvider(@"F:\root"))
{
    IFileInfo fileInfo = fileProvider.GetFileInfo("dir1/baz.txt");

    string content = "";

    using (var stream = fileInfo.CreateReadStream())
    {
        byte[] buffer = new byte[stream.Length];

        stream.Read(buffer, 0, buffer.Length);

        content = Encoding.Default.GetString(buffer);
    }

    Console.WriteLine(content);
}

1.4 示例3 - 监控文件变化

var fileProvider = new PhysicalFileProvider(@"F:\root");

ChangeToken.OnChange(() => fileProvider.Watch("dir1/baz.txt"), callback);

void callback()
{
    IFileInfo fileInfo = fileProvider.GetFileInfo("dir1/baz.txt");

    string content = "";

    using (var stream = fileInfo.CreateReadStream())
    {
        byte[] buffer = new byte[stream.Length];

        stream.Read(buffer, 0, buffer.Length);

        content = Encoding.Default.GetString(buffer);
    }

    Console.WriteLine(content);
}

int i = 0;
while (i++ < 10)
{
    File.WriteAllText(@"F:\root\dir1\baz.txt", DateTime.Now.ToString());

    await Task.Delay(1000);
}

输出结果:

2020/6/5 16:23:24
2020/6/5 16:23:24
2020/6/5 16:23:25
2020/6/5 16:23:26
2020/6/5 16:23:27
2020/6/5 16:23:28
2020/6/5 16:23:29
2020/6/5 16:23:30
2020/6/5 16:23:31
2020/6/5 16:23:32
2020/6/5 16:23:33

2. 模型解析

2.1 Globbing Pattern

Globbing Pattern表达式体现为一个文件路径,用于筛选目标目录或文件。Globbing Pattern表达式只包含2个通配符:

  • *: 代表所有不包含路径分隔符的所有字符。
  • **: 代表路径分隔符在内的所有字符。

下面列举几种常见的Globbing Pattern表达式:

Globbing Pattern表达式 匹配的文件
src/foobar/foo/settings.* 目录“src/foobar/foo/”下所有名为“settings”的文件,如settings.json、settigns.ini、settings.xml等
src/foobar/foo/*.cs 目录“src/foobar/foo/”下所有.cs文件
src/foobar/foo/ * .* 目录“src/foobar/foo/”下的所有文件
src/**/*.cs 目录"src"及其子目录下的所有.cs文件

2.2 核心接口

接口关系图

IFileProvider

IFileProvider是文件系统的核心接口,该接口定义了3个方法,体现了文件系统的3个基本功能。

public interface IFileProvider
{
    // 遍历目录
    IDirectoryContents GetDirectoryContents(string subpath);
    // 读取文件
    IFileInfo GetFileInfo(string subpath);
    // 监控文件变化
    IChangeToken Watch(string filter);
}
  • Watch方法监控目录或文件的变化。该方法接收一个Globbing Pattern字符串参数filter,用来筛选需要监控的目标文件。

IFileInfo

public interface IFileInfo
{
    bool Exists { get; }

    long Length { get; }

    string PhysicalPath { get; }

    string Name { get; }

    DateTimeOffset LastModified { get; }

    bool IsDirectory { get; }

    Stream CreateReadStream();
}
  1. 虽然文件系统采用目录组织文件,但不论文件还是目录都都通过一个IFileInfo对象表示。至于具体是目录还是文件,要通过IsDirectory属性来判断。
  2. 一般来说,不论指定的文件是否存在,GetFileInfo方法总是返回一个具体的IFileInfo对象。目标文件的存在与否由Exists属性决定。
  3. 可以借助CreateReadStream方法读取文件的内容。

IDirectoryContents

public interface IDirectoryContents : IEnumerable<IFileInfo>
{
    bool Exists { get; }
}

一个IDirectoryContents对象实际上是一组IFileInfo对象的集合。和GetFileInfo方法一样,不论指定的目录是否存在,GetDirectoryContents方法总是返回一个具体的IDirectoryContents对象,它的Exists属性可以确定目录是否存在。

IChangeToken

public interface IChangeToken
{
    // Gets a value that indicates if a change has occurred.
    bool HasChanged { get; }

    // Indicates if this token will pro-actively raise callbacks. If false, the token
    // consumer must poll Microsoft.Extensions.Primitives.IChangeToken.HasChanged to
    // detect changes.
    bool ActiveChangeCallbacks { get; }

    IDisposable RegisterChangeCallback(Action<object> callback, object state);
}

IChangeToken对象是一个监控数据的“令牌”,它能够在数据发生变更时及时对外发出一个通知。

我们可以调用RegisterChangeCallback方法注册一个回调函数,在数据发生变化时自动做出响应。该函数的返回值是一个IDisposable对象,调用Dispose方法可以解除注册的回调。

2.3 ChangeToken

IChangeToken 类图

CancellationChangeToken

.Net Core提供了若干原生的IChangeToken接口实现,我们最常用的是CancellationChangeToken。它的实现原理很简单,就是借助System.Threading.CancellationToken对象来发送通知的。

public class CancellationChangeToken : IChangeToken
{
    private readonly CancellationToken _token;
    // 参数:
    //   cancellationToken:
    //     The System.Threading.CancellationToken
    public CancellationChangeToken(CancellationToken cancellationToken)
        => _token = cancellationToken;

    public bool ActiveChangeCallbacks { get; } = true;
    public bool HasChanged { get; } = _token.IsCancellationRequested;

    public IDisposable RegisterChangeCallback(Action<object> callback, object state)
        => _token.Register(callback, state);
}

CompositeChangeToken

public class CompositeChangeToken : IChangeToken
{

    public CompositeChangeToken(IReadOnlyList<IChangeToken> changeTokens);

    public IReadOnlyList<IChangeToken> ChangeTokens { get; }
    public bool HasChanged { get; }
    public bool ActiveChangeCallbacks { get; }

    public IDisposable RegisterChangeCallback(Action<object> callback, object state);
}
  1. CompositeChangeToken代表由多个IChangeToken组合而成的复合型IChangeToken对象, 在创建CompositeChangeToken时需要提供这些IChangeToken
  2. 对于一个CompositeChangeToken对象来说,只要组成它的任何一个IChangeToken发生改变,其HasChanged属性就返回True, 并调用其注册的回调函数。
  3. 任何一个IChangeTokenActiveChangeCallbacks属性返回True, 则ActiveChangeCallbacks属性返回True。

ChangeToken

我们可以使用IChangeTokenRegisterChangeCallback方法来注册回调函数。但是这种方法存在一点不足:一个IChangeToken对象仅能发送一次通知。也就是说,如果想要连续不断的检测文件变化,就必须多次调用Watch方法并注册回调函数。

好在ChangeToken类帮我们解决了这个问题:OnChange方法会在一个IChangeToken对象发送通知后再次添加监控。

public static class ChangeToken
{
    public static IDisposable OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer);
    public static IDisposable OnChange<TState>(Func<IChangeToken> changeTokenProducer, 
        ction<TState> changeTokenConsumer, TState state);
}

使用示例:

ChangeToken.OnChange(() => fileProvider.Watch("dir1/*.txt"), ()=>{ ... });

3. 物理文件系统

类关系图

3.1 PhysicalFileProvider

物理文件系统是由PhysicalFileProvider构建的。一个PhysicalFileProvider总是映射到一个具体的物理目录上,并把该目录作为文件系统的根目录。

public class PhysicalFileProvider : IFileProvider, IDisposable
{
    public PhysicalFileProvider(string root);
    public PhysicalFileProvider(string root, ExclusionFilters filters);

    public string Root { get; }

    public IDirectoryContents GetDirectoryContents(string subpath);
    public IFileInfo GetFileInfo(string subpath);
    public IChangeToken Watch(string filter);

    ...
}

3.2 获取文件

PhysicalFileInfo

如果存在指定的物理文件,则GetFileInfo方法返回一个PhysicalFileInfo对象。该对象实际上是对System.IO.FileInfo的封装。

public class PhysicalFileInfo : IFileInfo
{
    public PhysicalFileInfo(FileInfo info);
    ...
}

NotFoundFileInfo

PhysicalFileProvider会将以下场景视为“目标文件不存在”,并让GetFileInfo方法返回NotFoundFileInfo对象。

  • 确实不存在一个物理文件与指定的路径相匹配。
  • 指定的是绝对路径。
  • 路径指向一个隐藏文件。
public class NotFoundFileInfo : IFileInfo
{
    public bool Exists { get; } = false;
    ...
}

PhysicalDirectoryInfo

如果指向的路径代表一个目录,则GetFileInfo方法返回PhysicalDirectoryInfo对象。该对象是对System.IO.DirectoryInfo的封装。

public class PhysicalDirectoryInfo : IFileInfo
{
    public PhysicalDirectoryInfo(DirectoryInfo info);
    ...
}

3.3 遍历目录

PhysicalDirectoryContents

调用GetDirectoryContents方法时,如果路径指向一个存在的目录,则返回PhysicalDirectoryContents对象。

public class PhysicalDirectoryContents : IDirectoryContents, IEnumerable<IFileInfo>
{
    public PhysicalDirectoryContents(string directory);

    public bool Exists { get; } 

    ...
}

NotFoundDirectoryContents

如果路径指向一个不存在的目录,或者是一个绝对路径,则返回NotFoundDirectoryContents对象。

public class NotFoundDirectoryContents : IDirectoryContents, IEnumerable<IFileInfo>
{
    public static NotFoundDirectoryContents Singleton { get; }
    public bool Exists { get; } = false;
    ...
}

3.4 文件监控

PhysicalFilesWatcher

PhysicalFileProvider利用PhysicalFilesWatcher来实现文件监控。文件或目录的任何变化(创建、修改、重命名和删除)都会实时地反映到IChangeToken对象上。

public class PhysicalFilesWatcher : IDisposable
{
    public PhysicalFilesWatcher(string root, FileSystemWatcher fileSystemWatcher, bool pollForChanges);
    public IChangeToken CreateFileChangeToken(string filter);
    ...
}

从构造函数可以看出,PhysicalFilesWatcher是利用System.IO.FileSystemWatcher来实现文件监控功能的。

4. 嵌入式文件系统

4.1 内嵌资源介绍

4.1.1 添加内嵌资源

如果需要把资源内嵌至程序集中,就需要修改当前项目的.csproj文件。通过添加<EmbeddedResource>,把对应的文件包含进来。

现将如下目录结构的文件内嵌至程序集中(root位于项目的根目录下):

root
├─ dir1/
│  ├─ footbar/
│  │  └─ bar.txt
│  └─ baz.txt
├─dir2/
│  └─ quz.txt
└─ coding.txt

修改项目的.csproj文件:

<Project Sdk="Microsoft.NET.Sdk">
...

<ItemGroup>
  <EmbeddedResource Include="root\**"  Exclude=""/>
</ItemGroup>

...
</Project>

<EmbeddedResource>具有两个属性,每个属性的值都可以使用Globbing Pattern表达式:

  • Include: 添加内嵌资源文件
  • Exclude: 排除不符合要求的文件。

4.1.2 文件命名空间

每个程序集都有一个清单文件(Manifest),其作用是记录组成程序集的所有文件。如果将上述文件文件内嵌到程序集中,清单文件将采用如下方式来记录它们。

.mresource public App.root.dir1.footbar.bar.txt
{
    // Offset: 0x00000000 Length: 0x0000000C
}
.mresource public App.root.dir1.baz.txt
{
    // Offset: 0x00000020 Length: 0x0000000C
}
.mresource public App.root.dir2.quz.txt
{
    // Offset: 0x00000030 Length: 0x0000000C
}
.mresource public App.root.coding.txt
{
    // Offset: 0x00000050 Length: 0x0000000C
}

虽然文件在原始项目中具有层次化的目录结构,但是在编译生成的程序集中,目录结构将不复存在,所有的文件按照命名空间的形式存放在同一个容器中。在生成程序集的过程中,编译器会按照一定的规则对文件进行重命名。

其规则是:{BaseNamespace}.{Path},目录分隔符(“/”)将替换成“.”。

::: tip BaseNamespace
这里的BaseNamespace不是程序集的名称,而是项目设置的默认命名空间的名称。
BaseNamespace
:::

4.1.3 读取资源文件

表示程序集的Assembly对象定义了如下几个方法,用来操作内嵌资源文件。

public abstract class Assembly
{
    // 获取记录在程序集Manifest中的资源文件名
    public virtual string[] GetManifestResourceNames();
    // 获取指定资源文件的描述信息
    public virtual ManifestResourceInfo? GetManifestResourceInfo(string resourceName);
    // 返回一个读取文件内容的stream对象
    public virtual Stream? GetManifestResourceStream(string name);
    ...
}

4.2 EmbeddedFileProvider

通过EmbeddedFileProvider构建的文件系统没有目录层级的概念,我们可以认为所有的资源文件都保存在根目录下。

public class EmbeddedFileProvider : IFileProvider
{
    public EmbeddedFileProvider(Assembly assembly);
    public EmbeddedFileProvider(Assembly assembly, string baseNamespace);

    public IDirectoryContents GetDirectoryContents(string subpath);
    public IFileInfo GetFileInfo(string subpath);
    public IChangeToken Watch(string pattern);
}

::: tip baseNamespace
构建EmbeddedFileProvider时如果没有指定baseNamespace, 则采用程序集的名称作为命名空间。
:::

示例:

Console.WriteLine("================默认baseNamespace");
var provider = new EmbeddedFileProvider(Assembly.GetExecutingAssembly());

var contents = provider.GetDirectoryContents("/");

foreach (var info in contents)
{
    Console.WriteLine(info.Name);
}

Console.WriteLine("================设置baseNamespace");
provider = new EmbeddedFileProvider(Assembly.GetExecutingAssembly(), "FileProviderTutorial.root.dir1");

contents = provider.GetDirectoryContents("/");

foreach (var info in contents)
{
    Console.WriteLine(info.Name);
}

输出:

================默认baseNamespace
root.bzx.txt
root.dir1.baz.txt
root.dir1.footbar.bar.txt
root.dir2.quz.txt
================设置baseNamespace
baz.txt
footbar.bar.txt

4.3 ManifestEmbeddedFileProvider

EmbeddedFileProvider的功能基本一致,不同点在于:它根据清单来重建嵌入文件的原始路径。也就是说,ManifestEmbeddedFileProvider构建的文件系统是有目录结构的。

public class ManifestEmbeddedFileProvider : IFileProvider
{
    public ManifestEmbeddedFileProvider(Assembly assembly);
    public ManifestEmbeddedFileProvider(Assembly assembly, string root);

    public IDirectoryContents GetDirectoryContents(string subpath);
    public IFileInfo GetFileInfo(string subpath);
    public IChangeToken Watch(string filter);
}

5 其他文件系统

5.1 复合型文件系统

5.1.1 CompositeFileProvider

CompositeFileProvider代表由多个IFileProvider构建的复合型文件系统。当创建一个CompositeFileProvider对象实例时,需要提供一组IFileProvider对象。

public class CompositeFileProvider : IFileProvider
{
    public CompositeFileProvider(params IFileProvider[] fileProviders);
    public CompositeFileProvider(IEnumerable<IFileProvider> fileProviders);

    public IEnumerable<IFileProvider> FileProviders { get; }

    public IDirectoryContents GetDirectoryContents(string subpath);
    public IFileInfo GetFileInfo(string subpath);
    public IChangeToken Watch(string pattern);
}

5.1.2 GetFileInfo

当调用GetFileInfo方法时,它会遍历这些IFileProvider对象,直到找到一个与路径相匹配的文件。如果所有的IFileProvider都不能提供这个文件,则返回NotFoundFileInfo对象。

由于遍历的顺序取决于构建CompositeFileProvider对象时提供的IFileProvider的顺序,所以如果对IFileProvider具有优先级的要求,应该将优先级高的IFileProvider对象放在前面。

5.1.3 CompositeDirectoryContents

GetDirectoryContents方法返回一个复合型的IDirectoryContents,即CompositeDirectoryContents。如果多个IFileProvider中存在路径相同的文件,与GetFileInfo一样,总是从优先提供的IFileProvider中提取。

public class CompositeDirectoryContents : IDirectoryContents, IEnumerable<IFileInfo>
{
    public CompositeDirectoryContents(IList<IFileProvider> fileProviders, string subpath);
    public bool Exists { get; }
}

5.1.4 CompositeChangeToken

Watch方法也返回一个复合型对象,即CompositeChangeToken。这个对象由所有的IFileProvider调用Watch方法来构建,所以它能监控所有的IFileProvider对象对应的文件系统。

public class CompositeChangeToken : IChangeToken
{
    public CompositeChangeToken(IReadOnlyList<IChangeToken> changeTokens);

    ...
}

5.2 空文件系统

NullFileProvider代表一个不包含任何内容的空文件系统。

public class NullFileProvider : IFileProvider
{
    public IDirectoryContents GetDirectoryContents(string subpath) => new NotFoundDirectoryContents();
    public IFileInfo GetFileInfo(string subpath) => new NotFoundFileInfo(subpath);
    public IChangeToken Watch(string filter) => NullChangeToken.Singleton;
}
  • GetDirectoryContents 返回NotFoundDirectoryContents对象。
  • GetFileInfo 返回NotFoundFileInfo对象。
  • Watch返回NullChangeToken对象。
posted @ 2022-08-27 15:12  renzhsh  阅读(640)  评论(0编辑  收藏  举报