hugo/hugofs/component_fs.go
2024-02-18 12:16:30 +01:00

272 lines
6.2 KiB
Go

// Copyright 2024 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 (
iofs "io/fs"
"os"
"path"
"runtime"
"sort"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/spf13/afero"
"golang.org/x/text/unicode/norm"
)
// NewComponentFs creates a new component filesystem.
func NewComponentFs(opts ComponentFsOptions) *componentFs {
if opts.Component == "" {
panic("ComponentFsOptions.PathParser.Component must be set")
}
if opts.Fs == nil {
panic("ComponentFsOptions.Fs must be set")
}
bfs := NewBasePathFs(opts.Fs, opts.Component)
return &componentFs{Fs: bfs, opts: opts}
}
var _ FilesystemUnwrapper = (*componentFs)(nil)
// componentFs is a filesystem that holds one of the Hugo components, e.g. content, layouts etc.
type componentFs struct {
afero.Fs
opts ComponentFsOptions
}
func (fs *componentFs) UnwrapFilesystem() afero.Fs {
return fs.Fs
}
type componentFsDir struct {
*noOpRegularFileOps
DirOnlyOps
name string // the name passed to Open
fs *componentFs
}
// ReadDir reads count entries from this virtual directory and
// sorts the entries according to the component filesystem rules.
func (f *componentFsDir) ReadDir(count int) ([]iofs.DirEntry, error) {
fis, err := f.DirOnlyOps.(iofs.ReadDirFile).ReadDir(-1)
if err != nil {
return nil, err
}
// Filter out any symlinks.
n := 0
for _, fi := range fis {
// IsDir will always be false for symlinks.
keep := fi.IsDir()
if !keep {
// This is unfortunate, but is the only way to determine if it is a symlink.
info, err := fi.Info()
if err != nil {
if herrors.IsNotExist(err) {
continue
}
return nil, err
}
if info.Mode()&os.ModeSymlink == 0 {
keep = true
}
}
if keep {
fis[n] = fi
n++
}
}
fis = fis[:n]
n = 0
for _, fi := range fis {
s := path.Join(f.name, fi.Name())
if _, ok := f.fs.applyMeta(fi, s); ok {
fis[n] = fi
n++
}
}
fis = fis[:n]
sort.Slice(fis, func(i, j int) bool {
fimi, fimj := fis[i].(FileMetaInfo), fis[j].(FileMetaInfo)
if fimi.IsDir() != fimj.IsDir() {
return fimi.IsDir()
}
fimim, fimjm := fimi.Meta(), fimj.Meta()
if fimim.ModuleOrdinal != fimjm.ModuleOrdinal {
switch f.fs.opts.Component {
case files.ComponentFolderI18n:
// The way the language files gets loaded means that
// we need to provide the least important files first (e.g. the theme files).
return fimim.ModuleOrdinal > fimjm.ModuleOrdinal
default:
return fimim.ModuleOrdinal < fimjm.ModuleOrdinal
}
}
pii, pij := fimim.PathInfo, fimjm.PathInfo
if pii != nil {
basei, basej := pii.Base(), pij.Base()
exti, extj := pii.Ext(), pij.Ext()
if f.fs.opts.Component == files.ComponentFolderContent {
// Pull bundles to the top.
if pii.IsBundle() != pij.IsBundle() {
return pii.IsBundle()
}
}
if exti != extj {
// This pulls .md above .html.
return exti > extj
}
if basei != basej {
return basei < basej
}
}
if fimim.Weight != fimjm.Weight {
return fimim.Weight > fimjm.Weight
}
return fimi.Name() < fimj.Name()
})
return fis, nil
}
func (f *componentFsDir) Stat() (iofs.FileInfo, error) {
fi, err := f.DirOnlyOps.Stat()
if err != nil {
return nil, err
}
fim, _ := f.fs.applyMeta(fi, f.name)
return fim, nil
}
func (fs *componentFs) Stat(name string) (os.FileInfo, error) {
fi, err := fs.Fs.Stat(name)
if err != nil {
return nil, err
}
fim, _ := fs.applyMeta(fi, name)
return fim, nil
}
func (fs *componentFs) applyMeta(fi FileNameIsDir, name string) (FileMetaInfo, bool) {
if runtime.GOOS == "darwin" {
name = norm.NFC.String(name)
}
fim := fi.(FileMetaInfo)
meta := fim.Meta()
pi := fs.opts.PathParser.Parse(fs.opts.Component, name)
if pi.Disabled() {
return fim, false
}
if meta.Lang != "" {
if isLangDisabled := fs.opts.PathParser.IsLangDisabled; isLangDisabled != nil && isLangDisabled(meta.Lang) {
return fim, false
}
}
meta.PathInfo = pi
if !fim.IsDir() {
if fileLang := meta.PathInfo.Lang(); fileLang != "" {
// A valid lang set in filename.
// Give priority to myfile.sv.txt inside the sv filesystem.
meta.Weight++
meta.Lang = fileLang
}
}
if meta.Lang == "" {
meta.Lang = fs.opts.DefaultContentLanguage
}
langIdx, found := fs.opts.PathParser.LanguageIndex[meta.Lang]
if !found {
panic("no language found for " + meta.Lang)
}
meta.LangIndex = langIdx
if fi.IsDir() {
meta.OpenFunc = func() (afero.File, error) {
return fs.Open(name)
}
}
return fim, true
}
func (f *componentFsDir) Readdir(count int) ([]os.FileInfo, error) {
panic("not supported: Use ReadDir")
}
func (f *componentFsDir) Readdirnames(count int) ([]string, error) {
dirsi, err := f.DirOnlyOps.(iofs.ReadDirFile).ReadDir(count)
if err != nil {
return nil, err
}
dirs := make([]string, len(dirsi))
for i, d := range dirsi {
dirs[i] = d.Name()
}
return dirs, nil
}
type ComponentFsOptions struct {
// The filesystem where one or more components are mounted.
Fs afero.Fs
// The component name, e.g. "content", "layouts" etc.
Component string
DefaultContentLanguage string
// The parser used to parse paths provided by this filesystem.
PathParser *paths.PathParser
}
func (fs *componentFs) Open(name string) (afero.File, error) {
f, err := fs.Fs.Open(name)
if err != nil {
return nil, err
}
fi, err := f.Stat()
if err != nil {
if err != errIsDir {
f.Close()
return nil, err
}
} else if !fi.IsDir() {
return f, nil
}
return &componentFsDir{
DirOnlyOps: f,
name: name,
fs: fs,
}, nil
}
func (fs *componentFs) ReadDir(name string) ([]os.FileInfo, error) {
panic("not implemented")
}