深入理解Hugo: OverlayFs

package main
import (
    "bytes"
    "fmt"
    "golang.org/x/tools/txtar"
    "io"
    "io/fs"
    "io/ioutil"
    "os"
    "path"
    "path/filepath"
    "sort"
)
func main() {
    dir, _ := os.MkdirTemp("", "hugo")
    defer os.RemoveAll(dir)
    ofs := New([]AbsStatFss{
        projectModuleFs(dir), mythemeModuleFs(dir)})
    a, _ := ofs.Open("a.md")
    defer a.Close()
    b, _ := ofs.Open("b.md")
    defer b.Close()
    c, _ := ofs.Open("c.md")
    defer c.Close()
    fmt.Println(readFile(a))
    fmt.Println(readFile(b))
    fmt.Println(readFile(c))
    fis, _ := ofs.ReadDir(".")
    for _, fi := range fis {
        fmt.Println(fi.Name())
    }
}
func projectModuleFs(dir string) AbsStatFss {
    modulePath := filepath.Join(dir, "project")
    _ = os.Mkdir(modulePath, os.ModePerm)
    ps := "-- a.md --\n" +
        "project: a\n" +
        "-- c.md --\n" +
        "project: c"
    writeFiles(ps, modulePath)
    return &moduleFs{
        workingDir: modulePath,
    }
}
func mythemeModuleFs(dir string) AbsStatFss {
    modulePath := filepath.Join(dir, "mytheme")
    _ = os.Mkdir(modulePath, os.ModePerm)
    ms := "-- a.md --\n" +
        "mytheme: a\n" +
        "-- b.md --\n" +
        "mytheme: b"
    writeFiles(ms, modulePath)
    return &moduleFs{
        workingDir: modulePath,
    }
}
func writeFiles(s string, dir string) {
    data := txtar.Parse([]byte(s))
    for _, f := range data.Files {
        if err := os.WriteFile(
            filepath.Join(dir, f.Name),
            bytes.TrimSuffix(f.Data, []byte("\n")),
            os.ModePerm); err != nil {
            panic(err)
        }
    }
}
type moduleFs struct {
    workingDir string
}
func (m *moduleFs) Abs(name string) []string {
    return []string{path.Join(m.workingDir, name)}
}
func (m *moduleFs) Fss() []fs.StatFS {
    return []fs.StatFS{os.DirFS(m.workingDir).(fs.StatFS)}
}

Abs returns an absolute path of file or dir.

type AbsStatFss interface {
    Abs(name string) []string
    Fss() []fs.StatFS
}

OverlayFs is a filesystem that overlays multiple filesystems. It’s by default a read-only filesystem. For all operations, the filesystems are checked in order until found.

type OverlayFs struct {
    fss []AbsStatFss
}

New creates a new OverlayFs with the given options.

func New(fss []AbsStatFss) *OverlayFs {
    return &OverlayFs{
        fss: fss,
    }
}
func (ofs OverlayFs) Append(
    fss ...AbsStatFss) *OverlayFs {
    ofs.fss = append(ofs.fss, fss...)
    return &ofs
}

Open opens a file, returning it or an error, if any happens. If name is a directory, a *Dir is returned representing all directories matching name. Note that a *Dir must not be used after it’s closed.

func (ofs *OverlayFs) Open(name string) (fs.File, error) {
    bfs, fi, err := ofs.stat(name)
    if err != nil {
        return nil, err
    }
    if fi.IsDir() {
        return nil, os.ErrInvalid
    }
    return bfs.Open(name)
}
func (ofs *OverlayFs) Stat(
    name string) (os.FileInfo, error) {
    _, fi, err := ofs.stat(name)
    return fi, err
}
func (ofs *OverlayFs) stat(
    name string) (fs.StatFS, os.FileInfo, error) {
    for _, bfs := range ofs.fss {
        fss := bfs.Fss()
        for _, sfs := range fss {
            if fi, err := sfs.Stat(name); err == nil ||
                !os.IsNotExist(err) {
                return sfs, fi, nil
            }
        }
    }
    return nil, nil, os.ErrNotExist
}
func readFile(f fs.File) string {
    b, _ := io.ReadAll(f)
    return string(b)
}
func (ofs *OverlayFs) ReadDir(
    dirname string) ([]fs.FileInfo, error) {
    var fis []fs.FileInfo
    readDir := func(bfs AbsStatFss) error {
        dirs := bfs.Abs(dirname)
        for _, dir := range dirs {
            files, err := ioutil.ReadDir(dir)
            if err != nil {
                return err
            }
            fis = merge(fis, files)
        }
        return nil
    }
    for _, bfs := range ofs.fss {
        if err := readDir(bfs); err != nil {
            return nil, err
        }
    }
    sort.Slice(fis, func(i, j int) bool {
        return fis[i].Name() < fis[j].Name()
    })
    return fis, nil
}
func merge(upper, lower []fs.FileInfo) []fs.FileInfo {
    for _, lfi := range lower {
        var found bool
        for _, ufi := range upper {
            if lfi.Name() == ufi.Name() {
                found = true
                break
            }
        }
        if !found {
            upper = append(upper, lfi)
        }
    }
    return upper
}

OverlayFs based on golang foundation package fs.Fs Result of merge layer

project: a
mytheme: b
project: c
a.md
b.md
c.md
Program exited.

Next example: OverlayFs Afero.