mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
7ff0a8ee9f
This is preparation for #6041. For historic reasons, the code for bulding the section tree and the taxonomies were very much separate. This works, but makes it hard to extend, maintain, and possibly not so fast as it could be. This simplification also introduces 3 slightly breaking changes, which I suspect most people will be pleased about. See referenced issues: This commit also switches the radix tree dependency to a mutable implementation: github.com/armon/go-radix. Fixes #6154 Fixes #6153 Fixes #6152
557 lines
12 KiB
Go
557 lines
12 KiB
Go
// Copyright 2019 The Hugo Authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package hugofs
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gohugoio/hugo/hugofs/files"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
radix "github.com/armon/go-radix"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
var filepathSeparator = string(filepath.Separator)
|
|
|
|
// NewRootMappingFs creates a new RootMappingFs on top of the provided with
|
|
// of root mappings with some optional metadata about the root.
|
|
// Note that From represents a virtual root that maps to the actual filename in To.
|
|
func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) {
|
|
rootMapToReal := radix.New()
|
|
|
|
for _, rm := range rms {
|
|
(&rm).clean()
|
|
|
|
fromBase := files.ResolveComponentFolder(rm.From)
|
|
if fromBase == "" {
|
|
panic("unrecognised component folder in" + rm.From)
|
|
}
|
|
|
|
if len(rm.To) < 2 {
|
|
panic(fmt.Sprintf("invalid root mapping; from/to: %s/%s", rm.From, rm.To))
|
|
}
|
|
|
|
_, err := fs.Stat(rm.To)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Extract "blog" from "content/blog"
|
|
rm.path = strings.TrimPrefix(strings.TrimPrefix(rm.From, fromBase), filepathSeparator)
|
|
|
|
key := rm.rootKey()
|
|
var mappings []RootMapping
|
|
v, found := rootMapToReal.Get(key)
|
|
if found {
|
|
// There may be more than one language pointing to the same root.
|
|
mappings = v.([]RootMapping)
|
|
}
|
|
mappings = append(mappings, rm)
|
|
rootMapToReal.Insert(key, mappings)
|
|
}
|
|
|
|
rfs := &RootMappingFs{Fs: fs,
|
|
virtualRoots: rms,
|
|
rootMapToReal: rootMapToReal}
|
|
|
|
return rfs, nil
|
|
}
|
|
|
|
// NewRootMappingFsFromFromTo is a convenicence variant of NewRootMappingFs taking
|
|
// From and To as string pairs.
|
|
func NewRootMappingFsFromFromTo(fs afero.Fs, fromTo ...string) (*RootMappingFs, error) {
|
|
rms := make([]RootMapping, len(fromTo)/2)
|
|
for i, j := 0, 0; j < len(fromTo); i, j = i+1, j+2 {
|
|
rms[i] = RootMapping{
|
|
From: fromTo[j],
|
|
To: fromTo[j+1],
|
|
}
|
|
}
|
|
|
|
return NewRootMappingFs(fs, rms...)
|
|
}
|
|
|
|
type RootMapping struct {
|
|
From string
|
|
To string
|
|
|
|
path string // The virtual mount point, e.g. "blog".
|
|
Meta FileMeta // File metadata (lang etc.)
|
|
}
|
|
|
|
func (rm *RootMapping) clean() {
|
|
rm.From = strings.Trim(filepath.Clean(rm.From), filepathSeparator)
|
|
rm.To = filepath.Clean(rm.To)
|
|
}
|
|
|
|
func (r RootMapping) filename(name string) string {
|
|
if name == "" {
|
|
return r.To
|
|
}
|
|
return filepath.Join(r.To, strings.TrimPrefix(name, r.From))
|
|
}
|
|
|
|
func (r RootMapping) rootKey() string {
|
|
return r.From
|
|
}
|
|
|
|
// A RootMappingFs maps several roots into one. Note that the root of this filesystem
|
|
// is directories only, and they will be returned in Readdir and Readdirnames
|
|
// in the order given.
|
|
type RootMappingFs struct {
|
|
afero.Fs
|
|
rootMapToReal *radix.Tree
|
|
virtualRoots []RootMapping
|
|
filter func(r RootMapping) bool
|
|
}
|
|
|
|
func (fs *RootMappingFs) Dirs(base string) ([]FileMetaInfo, error) {
|
|
roots := fs.getRootsWithPrefix(base)
|
|
|
|
if roots == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
fss := make([]FileMetaInfo, len(roots))
|
|
for i, r := range roots {
|
|
bfs := afero.NewBasePathFs(fs.Fs, r.To)
|
|
bfs = decoratePath(bfs, func(name string) string {
|
|
p := strings.TrimPrefix(name, r.To)
|
|
if r.path != "" {
|
|
// Make sure it's mounted to a any sub path, e.g. blog
|
|
p = filepath.Join(r.path, p)
|
|
}
|
|
p = strings.TrimLeft(p, filepathSeparator)
|
|
return p
|
|
})
|
|
fs := decorateDirs(bfs, r.Meta)
|
|
fi, err := fs.Stat("")
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "RootMappingFs.Dirs")
|
|
}
|
|
fss[i] = fi.(FileMetaInfo)
|
|
}
|
|
|
|
return fss, nil
|
|
}
|
|
|
|
// LstatIfPossible returns the os.FileInfo structure describing a given file.
|
|
func (fs *RootMappingFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
|
|
fis, _, b, err := fs.doLstat(name, false)
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
return fis[0], b, nil
|
|
}
|
|
|
|
func (fs *RootMappingFs) virtualDirOpener(name string, isRoot bool) func() (afero.File, error) {
|
|
return func() (afero.File, error) { return &rootMappingFile{name: name, isRoot: isRoot, fs: fs}, nil }
|
|
}
|
|
|
|
func (fs *RootMappingFs) doLstat(name string, allowMultiple bool) ([]FileMetaInfo, []FileMetaInfo, bool, error) {
|
|
|
|
if fs.isRoot(name) {
|
|
return []FileMetaInfo{newDirNameOnlyFileInfo(name, true, fs.virtualDirOpener(name, true))}, nil, false, nil
|
|
}
|
|
|
|
roots := fs.getRoots(name)
|
|
rootsWithPrefix := fs.getRootsWithPrefix(name)
|
|
hasRootMappingsBelow := len(rootsWithPrefix) != 0
|
|
|
|
if len(roots) == 0 {
|
|
if hasRootMappingsBelow {
|
|
// No exact matches, but we have root mappings below name,
|
|
// let's make it look like a directory.
|
|
return []FileMetaInfo{newDirNameOnlyFileInfo(name, true, fs.virtualDirOpener(name, false))}, nil, false, nil
|
|
}
|
|
|
|
return nil, nil, false, os.ErrNotExist
|
|
}
|
|
|
|
// We may have a mapping for both static and static/subdir.
|
|
// These will not show in any Readdir so append them
|
|
// manually.
|
|
rootsInDir := fs.filterRootsBelow(rootsWithPrefix, name)
|
|
|
|
var (
|
|
fis []FileMetaInfo
|
|
dirs []FileMetaInfo
|
|
b bool
|
|
fi os.FileInfo
|
|
root RootMapping
|
|
err error
|
|
)
|
|
|
|
for _, root = range roots {
|
|
fi, b, err = fs.statRoot(root, name)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return nil, nil, false, err
|
|
}
|
|
fim := fi.(FileMetaInfo)
|
|
fis = append(fis, fim)
|
|
}
|
|
|
|
for _, root = range rootsInDir {
|
|
fi, _, err := fs.statRoot(root, "")
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return nil, nil, false, err
|
|
}
|
|
fim := fi.(FileMetaInfo)
|
|
dirs = append(dirs, fim)
|
|
}
|
|
|
|
if len(fis) == 0 && len(dirs) == 0 {
|
|
return nil, nil, false, os.ErrNotExist
|
|
}
|
|
|
|
if allowMultiple || len(fis) == 1 {
|
|
return fis, dirs, b, nil
|
|
}
|
|
|
|
// Open it in this composite filesystem.
|
|
opener := func() (afero.File, error) {
|
|
return fs.Open(name)
|
|
}
|
|
|
|
return []FileMetaInfo{decorateFileInfo(fi, fs, opener, "", "", root.Meta)}, nil, b, nil
|
|
|
|
}
|
|
|
|
// Open opens the namedrootMappingFile file for reading.
|
|
func (fs *RootMappingFs) Open(name string) (afero.File, error) {
|
|
if fs.isRoot(name) {
|
|
return &rootMappingFile{name: name, fs: fs, isRoot: true}, nil
|
|
}
|
|
|
|
fis, dirs, _, err := fs.doLstat(name, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(fis) == 1 {
|
|
fi := fis[0]
|
|
meta := fi.(FileMetaInfo).Meta()
|
|
f, err := meta.Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
f = &rootMappingFile{File: f, fs: fs, name: name, meta: meta}
|
|
|
|
if len(dirs) > 0 {
|
|
return &readDirDirsAppender{File: f, dirs: dirs}, nil
|
|
}
|
|
|
|
return f, nil
|
|
}
|
|
|
|
f, err := fs.newUnionFile(fis...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(dirs) > 0 {
|
|
return &readDirDirsAppender{File: f, dirs: dirs}, nil
|
|
}
|
|
|
|
return f, nil
|
|
|
|
}
|
|
|
|
// Stat returns the os.FileInfo structure describing a given file. If there is
|
|
// an error, it will be of type *os.PathError.
|
|
func (fs *RootMappingFs) Stat(name string) (os.FileInfo, error) {
|
|
fi, _, err := fs.LstatIfPossible(name)
|
|
return fi, err
|
|
|
|
}
|
|
|
|
// Filter creates a copy of this filesystem with the applied filter.
|
|
func (fs RootMappingFs) Filter(f func(m RootMapping) bool) *RootMappingFs {
|
|
fs.filter = f
|
|
return &fs
|
|
}
|
|
|
|
func (fs *RootMappingFs) isRoot(name string) bool {
|
|
return name == "" || name == filepathSeparator
|
|
|
|
}
|
|
|
|
func (fs *RootMappingFs) getRoots(name string) []RootMapping {
|
|
name = filepath.Clean(name)
|
|
_, v, found := fs.rootMapToReal.LongestPrefix(name)
|
|
if !found {
|
|
return nil
|
|
}
|
|
|
|
rm := v.([]RootMapping)
|
|
|
|
return fs.applyFilterToRoots(rm)
|
|
}
|
|
|
|
func (fs *RootMappingFs) applyFilterToRoots(rm []RootMapping) []RootMapping {
|
|
if fs.filter == nil {
|
|
return rm
|
|
}
|
|
|
|
var filtered []RootMapping
|
|
for _, m := range rm {
|
|
if fs.filter(m) {
|
|
filtered = append(filtered, m)
|
|
}
|
|
}
|
|
|
|
return filtered
|
|
}
|
|
|
|
func (fs *RootMappingFs) getRootsWithPrefix(prefix string) []RootMapping {
|
|
if fs.isRoot(prefix) {
|
|
return fs.virtualRoots
|
|
}
|
|
prefix = filepath.Clean(prefix)
|
|
var roots []RootMapping
|
|
|
|
fs.rootMapToReal.WalkPrefix(prefix, func(b string, v interface{}) bool {
|
|
roots = append(roots, v.([]RootMapping)...)
|
|
return false
|
|
})
|
|
|
|
return fs.applyFilterToRoots(roots)
|
|
}
|
|
|
|
// Filter out the mappings inside the name directory.
|
|
func (fs *RootMappingFs) filterRootsBelow(roots []RootMapping, name string) []RootMapping {
|
|
if len(roots) == 0 {
|
|
return nil
|
|
}
|
|
|
|
sepCount := strings.Count(name, filepathSeparator)
|
|
var filtered []RootMapping
|
|
for _, x := range roots {
|
|
if name == x.From {
|
|
continue
|
|
}
|
|
|
|
if strings.Count(x.From, filepathSeparator)-sepCount != 1 {
|
|
continue
|
|
}
|
|
|
|
filtered = append(filtered, x)
|
|
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func (fs *RootMappingFs) newUnionFile(fis ...FileMetaInfo) (afero.File, error) {
|
|
meta := fis[0].Meta()
|
|
f, err := meta.Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rf := &rootMappingFile{File: f, fs: fs, name: meta.Name(), meta: meta}
|
|
if len(fis) == 1 {
|
|
return rf, err
|
|
}
|
|
|
|
next, err := fs.newUnionFile(fis[1:]...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
uf := &afero.UnionFile{Base: rf, Layer: next}
|
|
|
|
uf.Merger = func(lofi, bofi []os.FileInfo) ([]os.FileInfo, error) {
|
|
// Ignore duplicate directory entries
|
|
seen := make(map[string]bool)
|
|
var result []os.FileInfo
|
|
|
|
for _, fis := range [][]os.FileInfo{bofi, lofi} {
|
|
for _, fi := range fis {
|
|
|
|
if fi.IsDir() && seen[fi.Name()] {
|
|
continue
|
|
}
|
|
|
|
if fi.IsDir() {
|
|
seen[fi.Name()] = true
|
|
}
|
|
|
|
result = append(result, fi)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
return uf, nil
|
|
|
|
}
|
|
|
|
func (fs *RootMappingFs) statRoot(root RootMapping, name string) (os.FileInfo, bool, error) {
|
|
filename := root.filename(name)
|
|
|
|
var b bool
|
|
var fi os.FileInfo
|
|
var err error
|
|
|
|
if ls, ok := fs.Fs.(afero.Lstater); ok {
|
|
fi, b, err = ls.LstatIfPossible(filename)
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
|
|
} else {
|
|
fi, err = fs.Fs.Stat(filename)
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
}
|
|
|
|
// Opens the real directory/file.
|
|
opener := func() (afero.File, error) {
|
|
return fs.Fs.Open(filename)
|
|
}
|
|
|
|
if fi.IsDir() {
|
|
if name == "" {
|
|
name = root.From
|
|
}
|
|
_, name = filepath.Split(name)
|
|
fi = newDirNameOnlyFileInfo(name, false, opener)
|
|
}
|
|
|
|
return decorateFileInfo(fi, fs.Fs, opener, "", "", root.Meta), b, nil
|
|
|
|
}
|
|
|
|
type rootMappingFile struct {
|
|
afero.File
|
|
fs *RootMappingFs
|
|
name string
|
|
meta FileMeta
|
|
isRoot bool
|
|
}
|
|
|
|
type readDirDirsAppender struct {
|
|
afero.File
|
|
dirs []FileMetaInfo
|
|
}
|
|
|
|
func (f *readDirDirsAppender) Readdir(count int) ([]os.FileInfo, error) {
|
|
fis, err := f.File.Readdir(count)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, dir := range f.dirs {
|
|
fis = append(fis, dir)
|
|
}
|
|
return fis, nil
|
|
|
|
}
|
|
|
|
func (f *readDirDirsAppender) Readdirnames(count int) ([]string, error) {
|
|
fis, err := f.Readdir(count)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return fileInfosToNames(fis), nil
|
|
}
|
|
|
|
func (f *rootMappingFile) Close() error {
|
|
if f.File == nil {
|
|
return nil
|
|
}
|
|
return f.File.Close()
|
|
}
|
|
|
|
func (f *rootMappingFile) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
func (f *rootMappingFile) Readdir(count int) ([]os.FileInfo, error) {
|
|
if f.File == nil {
|
|
dirsn := make([]os.FileInfo, 0)
|
|
roots := f.fs.getRootsWithPrefix(f.name)
|
|
seen := make(map[string]bool)
|
|
|
|
j := 0
|
|
for _, rm := range roots {
|
|
if count != -1 && j >= count {
|
|
break
|
|
}
|
|
|
|
opener := func() (afero.File, error) {
|
|
return f.fs.Open(rm.From)
|
|
}
|
|
|
|
name := rm.From
|
|
if !f.isRoot {
|
|
_, name = filepath.Split(rm.From)
|
|
}
|
|
|
|
if seen[name] {
|
|
continue
|
|
}
|
|
seen[name] = true
|
|
|
|
j++
|
|
|
|
fi := newDirNameOnlyFileInfo(name, false, opener)
|
|
if rm.Meta != nil {
|
|
mergeFileMeta(rm.Meta, fi.Meta())
|
|
}
|
|
|
|
dirsn = append(dirsn, fi)
|
|
}
|
|
return dirsn, nil
|
|
}
|
|
|
|
if f.File == nil {
|
|
panic(fmt.Sprintf("no File for %q", f.name))
|
|
}
|
|
|
|
fis, err := f.File.Readdir(count)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i, fi := range fis {
|
|
fis[i] = decorateFileInfo(fi, f.fs, nil, "", "", f.meta)
|
|
}
|
|
|
|
return fis, nil
|
|
}
|
|
|
|
func (f *rootMappingFile) Readdirnames(count int) ([]string, error) {
|
|
dirs, err := f.Readdir(count)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return fileInfosToNames(dirs), nil
|
|
}
|