深入理解Hugo - 基础架构中的文件系统

2,206 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情 ,祝福祖国,生日快乐 :)

文件系统的组织

Hugo目前主要的操作都是对本地文件进行的。 比如读取配置信息、模版信息、博客内容,写入站点信息等等。 因为这些操作都离不开文件系统,Hugo因此对文件信息的组织做了许多工作,以确保调用方良好的使用体验。

Afero

首先是选择基础文件系统afero.Fs:

Afero is a filesystem framework providing a simple, uniform and universal API interacting with any filesystem, as an abstraction layer providing interfaces, types and methods. Afero has an exceptionally clean interface and simple design without needless constructors or initialization methods.

Afero提供的服务基本和Golang原生Fs接口一致,其次兼容多操作系统,这样就既对语言兼容,还对系统兼容,并且使用体验还和原生一致。 确实为Hugo提供了良好的文件系统基础。

为了直观查看Hugo中多种Fs的关联,我们把afero.Fs标作如下结构,包含文件系统的基础操作样例:

4.1-hfs-afero.Fs.svg

Hugo架构中的Fs

我们再来回顾下,Hugo架构中Fs应用的场景:

4.0-hugo-arch-fs.svg

最先出现的是Fs,用来记录最贴近真实目录的文件系统,作为DpesCfg的参数,最终由HugoSites创建Deps的时候,透传给了Deps。 Deps是真正组织构建整个文件系统体系的模块,最终用PathSpec将前面的原始文件系统组织成了Hugo所需要的文件系统。 最终供给Template和Page相关操作使用。

HugoFs

我们先看基础的Fs,也就是HugoFs:

4.2-hgf-hugoFs.svg

可以看到,基础hugoFs包含输入源Source,和输出目标地址PublishDir,以及用来只读的WorkingDir。 并且第一项都是afero.Fs类型,所以颜色和afero.Fs颜色一致。 后面也会用来颜色来对不同类型的文件系统进行关联。

PathSpec

通过上面的Hugo架构图我们知道,最终HugoFs被传入Dpes,并由PathSpec来统一组织和管理所有路径相关信息:

4.3-hfs-PathSpec.svg

从上图看出PathSpec包含了hugoFs和Paths,还有另一个重要的BaseFs。

先看Paths,包含了基础文件系统,还包含了主题和工作目录信息,以及Modules相关的信息。 基于Fs和Path,PathSpec需要消化这些基础信息,并提供完整的文件系统服务。

BaseFs

通过准备好的基础信息hugoFs和Paths,BaseFs不仅要提供一些基础服务,比如源文件系统和发布目标文件系统,以及工作目录等相关信息。 还需要按Hugo要求的基础目录对文件进行组织,像Content, Data, i18n, Layouts, Archetypes, Assets。 并且要求严格按模块加载顺序,提供最终的文件服务,比如用户在工程目录增自定义了一些模板,需要覆盖主题里自带的模板时。

4.4-hfs-BaseFs.svg

如上图所示,BaseFs用SourceFilesystems来对基础目录进行组织,用theBigFs来提供最终合并文件系统服务。

SourceFilesystems

4.5-hfs-SourceFilesystems.svg

想要映射出Hugo的基础文件结构,Hugo设计出了对应的结构SourceFilesystems来表示,并用字段一一对应。 每一项又具有共同的特征,既SourceFilesystem。

大家可以回忆下,在上一节Hugo的模块中有提到,每一个模块是如何在Mount中存储这些信息的。

theBigFs

4.6-hgs-theBigFs.svg

多模块会生成多个相同结构的文件系统,谁在前,谁在后,由模块配置信息决定。 那最终如何合并这些文件系统呢? 由上图可以看出,Hugo给出的答案是Overlay,工作原理可参考Wikipedia OverlayFS

Overlay的组织是由filesystemCollector来进行的,用到了文件元数据FileMetaInfo来进行描述,方便相关的文件操作,如查询,排序等。 在生成最终状态的Overlay视图前,需要RootMappingFs来帮助组织按Content, Static等进行分类的文件系统。 最终由Collector来将对应的文件放到对应的集合中。

有了以上这些组织好的文件系统后,想想可能的应用场景?

文件系统场景一 - ContentSpec

// hugo-playground/deps/deps.go
// line 145
log.Process("New content Spec", "content converter provider inside")
contentSpec, err := helpers.NewContentSpec(cfg.Language, ps.BaseFs.Content.Fs)

准备好PathSpec后,ContentSpec的创建立马就用到了Content.Fs,也就是SourceFilesystem.Fs,依赖于theBigFs.overlayMountsContent。

文件系统场景二 - loadTemplates

// hugo-playground/tpl/tplimpl/template.go
// line 340
if err := helpers.SymbolicWalk(t.Layouts.Fs, "", walker); err != nil {
    if !os.IsNotExist(err) {
        return err
    }
    return nil
}

在加载用户自定义模板时,就用到了Layouts.Fs。 通过walker对文件系统中的模板文件进行相应的处理,依赖于b.theBigFs.overlayDirs[files.ComponentFolderLayouts]

结合以上文件系统的设计和应用,我们可以感受到Hugo文件系统的设计需求来自于自身的特点。 因为用到了模块的理念,以及模块基础结构的设计。 基础的hugoFs并不能满足Hugo在操作文件系统过程中的所有需求,因此需要进一步封装。

Hugo的做法是用PathSpec来组织所有信息,隐藏复杂度,抽象出BaseFs提供更为贴近使用场景的综合服务。 用SourceFilesystems组织出符合Hugo基础结构特点的直观服务,并用OverlayFs底层技术,实现了多文件系统合并的需求,最终支持到真正的实际使用场景。 包括提供文章内容服务的Content文件系统,和加载自定义模板时的Layouts文件系统,等等。