mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
6413559f75
This commit adds a new config setting: ```toml disableLanguages = ["fr"] ``` If this is a multilingual site: * No site for the French language will be created * French content pages will be ignored/not read * The French language configuration (menus etc.) will also be ignored This makes it possible to start translating new languages and turn it on when you're happy etc. Fixes #4297 Fixed #4329
696 lines
16 KiB
Go
696 lines
16 KiB
Go
// Copyright 2017-present 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 hugolib
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/gohugoio/hugo/helpers"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"github.com/gohugoio/hugo/source"
|
|
"github.com/spf13/afero"
|
|
jww "github.com/spf13/jwalterweatherman"
|
|
)
|
|
|
|
var errSkipCyclicDir = errors.New("skip potential cyclic dir")
|
|
|
|
type capturer struct {
|
|
// To prevent symbolic link cycles: Visit same folder only once.
|
|
seen map[string]bool
|
|
seenMu sync.Mutex
|
|
|
|
handler captureResultHandler
|
|
|
|
sourceSpec *source.SourceSpec
|
|
fs afero.Fs
|
|
logger *jww.Notepad
|
|
|
|
baseDir string
|
|
|
|
// Filenames limits the content to process to a list of filenames/directories.
|
|
// This is used for partial building in server mode.
|
|
filenames []string
|
|
|
|
// Used to determine how to handle content changes in server mode.
|
|
contentChanges *contentChangeMap
|
|
|
|
// Semaphore used to throttle the concurrent sub directory handling.
|
|
sem chan bool
|
|
}
|
|
|
|
func newCapturer(
|
|
logger *jww.Notepad,
|
|
sourceSpec *source.SourceSpec,
|
|
handler captureResultHandler,
|
|
contentChanges *contentChangeMap,
|
|
baseDir string, filenames ...string) *capturer {
|
|
|
|
numWorkers := 4
|
|
if n := runtime.NumCPU(); n > numWorkers {
|
|
numWorkers = n
|
|
}
|
|
|
|
c := &capturer{
|
|
sem: make(chan bool, numWorkers),
|
|
handler: handler,
|
|
sourceSpec: sourceSpec,
|
|
logger: logger,
|
|
contentChanges: contentChanges,
|
|
fs: sourceSpec.Fs.Source, baseDir: baseDir, seen: make(map[string]bool),
|
|
filenames: filenames}
|
|
|
|
return c
|
|
}
|
|
|
|
// Captured files and bundles ready to be processed will be passed on to
|
|
// these channels.
|
|
type captureResultHandler interface {
|
|
handleSingles(fis ...*fileInfo)
|
|
handleCopyFiles(filenames ...string)
|
|
captureBundlesHandler
|
|
}
|
|
|
|
type captureBundlesHandler interface {
|
|
handleBundles(b *bundleDirs)
|
|
}
|
|
|
|
type captureResultHandlerChain struct {
|
|
handlers []captureBundlesHandler
|
|
}
|
|
|
|
func (c *captureResultHandlerChain) handleSingles(fis ...*fileInfo) {
|
|
for _, h := range c.handlers {
|
|
if hh, ok := h.(captureResultHandler); ok {
|
|
hh.handleSingles(fis...)
|
|
}
|
|
}
|
|
}
|
|
func (c *captureResultHandlerChain) handleBundles(b *bundleDirs) {
|
|
for _, h := range c.handlers {
|
|
h.handleBundles(b)
|
|
}
|
|
}
|
|
|
|
func (c *captureResultHandlerChain) handleCopyFiles(filenames ...string) {
|
|
for _, h := range c.handlers {
|
|
if hh, ok := h.(captureResultHandler); ok {
|
|
hh.handleCopyFiles(filenames...)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *capturer) capturePartial(filenames ...string) error {
|
|
handled := make(map[string]bool)
|
|
|
|
for _, filename := range filenames {
|
|
dir, resolvedFilename, tp := c.contentChanges.resolveAndRemove(filename)
|
|
if handled[resolvedFilename] {
|
|
continue
|
|
}
|
|
|
|
handled[resolvedFilename] = true
|
|
|
|
switch tp {
|
|
case bundleLeaf:
|
|
if err := c.handleDir(resolvedFilename); err != nil {
|
|
return err
|
|
}
|
|
case bundleBranch:
|
|
if err := c.handleBranchDir(resolvedFilename); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
fi, _, err := c.getRealFileInfo(resolvedFilename)
|
|
if os.IsNotExist(err) {
|
|
// File has been deleted.
|
|
continue
|
|
}
|
|
|
|
// Just in case the owning dir is a new symlink -- this will
|
|
// create the proper mapping for it.
|
|
c.getRealFileInfo(dir)
|
|
|
|
f, active := c.newFileInfo(resolvedFilename, fi, tp)
|
|
if active {
|
|
c.copyOrHandleSingle(f)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *capturer) capture() error {
|
|
if len(c.filenames) > 0 {
|
|
return c.capturePartial(c.filenames...)
|
|
}
|
|
|
|
err := c.handleDir(c.baseDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *capturer) handleNestedDir(dirname string) error {
|
|
select {
|
|
case c.sem <- true:
|
|
var g errgroup.Group
|
|
|
|
g.Go(func() error {
|
|
defer func() {
|
|
<-c.sem
|
|
}()
|
|
return c.handleDir(dirname)
|
|
})
|
|
return g.Wait()
|
|
default:
|
|
// For deeply nested file trees, waiting for a semaphore wil deadlock.
|
|
return c.handleDir(dirname)
|
|
}
|
|
}
|
|
|
|
// This handles a bundle branch and its resources only. This is used
|
|
// in server mode on changes. If this dir does not (anymore) represent a bundle
|
|
// branch, the handling is upgraded to the full handleDir method.
|
|
func (c *capturer) handleBranchDir(dirname string) error {
|
|
files, err := c.readDir(dirname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var (
|
|
dirType bundleDirType
|
|
)
|
|
|
|
for _, fi := range files {
|
|
if !fi.IsDir() {
|
|
tp, _ := classifyBundledFile(fi.Name())
|
|
if dirType == bundleNot {
|
|
dirType = tp
|
|
}
|
|
|
|
if dirType == bundleLeaf {
|
|
return c.handleDir(dirname)
|
|
}
|
|
}
|
|
}
|
|
|
|
if dirType != bundleBranch {
|
|
return c.handleDir(dirname)
|
|
}
|
|
|
|
dirs := newBundleDirs(bundleBranch, c)
|
|
|
|
for _, fi := range files {
|
|
|
|
if fi.IsDir() {
|
|
continue
|
|
}
|
|
|
|
tp, isContent := classifyBundledFile(fi.Name())
|
|
|
|
f, active := c.newFileInfo(fi.filename, fi.FileInfo, tp)
|
|
if !active {
|
|
continue
|
|
}
|
|
if f.isOwner() {
|
|
dirs.addBundleHeader(f)
|
|
} else if !isContent {
|
|
// This is a partial update -- we only care about the files that
|
|
// is in this bundle.
|
|
dirs.addBundleFiles(f)
|
|
}
|
|
}
|
|
|
|
c.handler.handleBundles(dirs)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
func (c *capturer) handleDir(dirname string) error {
|
|
files, err := c.readDir(dirname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
type dirState int
|
|
|
|
const (
|
|
dirStateDefault dirState = iota
|
|
|
|
dirStateAssetsOnly
|
|
dirStateSinglesOnly
|
|
)
|
|
|
|
var (
|
|
fileBundleTypes = make([]bundleDirType, len(files))
|
|
|
|
// Start with the assumption that this dir contains only non-content assets (images etc.)
|
|
// If that is still true after we had a first look at the list of files, we
|
|
// can just copy the files to destination. We will still have to look at the
|
|
// sub-folders for potential bundles.
|
|
state = dirStateAssetsOnly
|
|
|
|
// Start with the assumption that this dir is not a bundle.
|
|
// A directory is a bundle if it contains a index content file,
|
|
// e.g. index.md (a leaf bundle) or a _index.md (a branch bundle).
|
|
bundleType = bundleNot
|
|
)
|
|
|
|
/* First check for any content files.
|
|
- If there are none, then this is a assets folder only (images etc.)
|
|
and we can just plainly copy them to
|
|
destination.
|
|
- If this is a section with no image etc. or similar, we can just handle it
|
|
as it was a single content file.
|
|
*/
|
|
var hasNonContent, isBranch bool
|
|
|
|
for i, fi := range files {
|
|
if !fi.IsDir() {
|
|
tp, isContent := classifyBundledFile(fi.Name())
|
|
fileBundleTypes[i] = tp
|
|
if !isBranch {
|
|
isBranch = tp == bundleBranch
|
|
}
|
|
|
|
if isContent {
|
|
// This is not a assets-only folder.
|
|
state = dirStateDefault
|
|
} else {
|
|
hasNonContent = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if isBranch && !hasNonContent {
|
|
// This is a section or similar with no need for any bundle handling.
|
|
state = dirStateSinglesOnly
|
|
}
|
|
|
|
if state > dirStateDefault {
|
|
return c.handleNonBundle(dirname, files, state == dirStateSinglesOnly)
|
|
}
|
|
|
|
var fileInfos = make([]*fileInfo, 0, len(files))
|
|
|
|
for i, fi := range files {
|
|
currentType := bundleNot
|
|
|
|
if !fi.IsDir() {
|
|
currentType = fileBundleTypes[i]
|
|
if bundleType == bundleNot && currentType != bundleNot {
|
|
bundleType = currentType
|
|
}
|
|
}
|
|
|
|
if bundleType == bundleNot && currentType != bundleNot {
|
|
bundleType = currentType
|
|
}
|
|
f, active := c.newFileInfo(fi.filename, fi.FileInfo, currentType)
|
|
if !active {
|
|
continue
|
|
}
|
|
|
|
fileInfos = append(fileInfos, f)
|
|
}
|
|
|
|
var todo []*fileInfo
|
|
|
|
if bundleType != bundleLeaf {
|
|
for _, fi := range fileInfos {
|
|
if fi.FileInfo().IsDir() {
|
|
// Handle potential nested bundles.
|
|
filename := fi.Filename()
|
|
if err := c.handleNestedDir(filename); err != nil {
|
|
return err
|
|
}
|
|
} else if bundleType == bundleNot || (!fi.isOwner() && fi.isContentFile()) {
|
|
// Not in a bundle.
|
|
c.copyOrHandleSingle(fi)
|
|
} else {
|
|
// This is a section folder or similar with non-content files in it.
|
|
todo = append(todo, fi)
|
|
}
|
|
}
|
|
} else {
|
|
todo = fileInfos
|
|
}
|
|
|
|
if len(todo) == 0 {
|
|
return nil
|
|
}
|
|
|
|
dirs, err := c.createBundleDirs(todo, bundleType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Send the bundle to the next step in the processor chain.
|
|
c.handler.handleBundles(dirs)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *capturer) handleNonBundle(
|
|
dirname string,
|
|
fileInfos []fileInfoName,
|
|
singlesOnly bool) error {
|
|
|
|
for _, fi := range fileInfos {
|
|
if fi.IsDir() {
|
|
if err := c.handleNestedDir(fi.filename); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if singlesOnly {
|
|
f, active := c.newFileInfo(fi.filename, fi, bundleNot)
|
|
if !active {
|
|
continue
|
|
}
|
|
c.handler.handleSingles(f)
|
|
} else {
|
|
c.handler.handleCopyFiles(fi.filename)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *capturer) copyOrHandleSingle(fi *fileInfo) {
|
|
if fi.isContentFile() {
|
|
c.handler.handleSingles(fi)
|
|
} else {
|
|
// These do not currently need any further processing.
|
|
c.handler.handleCopyFiles(fi.Filename())
|
|
}
|
|
}
|
|
|
|
func (c *capturer) createBundleDirs(fileInfos []*fileInfo, bundleType bundleDirType) (*bundleDirs, error) {
|
|
dirs := newBundleDirs(bundleType, c)
|
|
|
|
for _, fi := range fileInfos {
|
|
if fi.FileInfo().IsDir() {
|
|
var collector func(fis ...*fileInfo)
|
|
|
|
if bundleType == bundleBranch {
|
|
// All files in the current directory are part of this bundle.
|
|
// Trying to include sub folders in these bundles are filled with ambiguity.
|
|
collector = func(fis ...*fileInfo) {
|
|
for _, fi := range fis {
|
|
c.copyOrHandleSingle(fi)
|
|
}
|
|
}
|
|
} else {
|
|
// All nested files and directories are part of this bundle.
|
|
collector = func(fis ...*fileInfo) {
|
|
fileInfos = append(fileInfos, fis...)
|
|
}
|
|
}
|
|
err := c.collectFiles(fi.Filename(), collector)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
} else if fi.isOwner() {
|
|
// There can be more than one language, so:
|
|
// 1. Content files must be attached to its language's bundle.
|
|
// 2. Other files must be attached to all languages.
|
|
// 3. Every content file needs a bundle header.
|
|
dirs.addBundleHeader(fi)
|
|
}
|
|
}
|
|
|
|
for _, fi := range fileInfos {
|
|
if fi.FileInfo().IsDir() || fi.isOwner() {
|
|
continue
|
|
}
|
|
|
|
if fi.isContentFile() {
|
|
if bundleType != bundleBranch {
|
|
dirs.addBundleContentFile(fi)
|
|
}
|
|
} else {
|
|
dirs.addBundleFiles(fi)
|
|
}
|
|
}
|
|
|
|
return dirs, nil
|
|
}
|
|
|
|
func (c *capturer) collectFiles(dirname string, handleFiles func(fis ...*fileInfo)) error {
|
|
filesInDir, err := c.readDir(dirname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, fi := range filesInDir {
|
|
if fi.IsDir() {
|
|
err := c.collectFiles(fi.filename, handleFiles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
f, active := c.newFileInfo(fi.filename, fi.FileInfo, bundleNot)
|
|
if active {
|
|
handleFiles(f)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *capturer) readDir(dirname string) ([]fileInfoName, error) {
|
|
if c.sourceSpec.IgnoreFile(dirname) {
|
|
return nil, nil
|
|
}
|
|
|
|
dir, err := c.fs.Open(dirname)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer dir.Close()
|
|
names, err := dir.Readdirnames(-1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fis := make([]fileInfoName, 0, len(names))
|
|
|
|
for _, name := range names {
|
|
filename := filepath.Join(dirname, name)
|
|
if !c.sourceSpec.IgnoreFile(filename) {
|
|
fi, _, err := c.getRealFileInfo(filename)
|
|
|
|
if err != nil {
|
|
// It may have been deleted in the meantime.
|
|
if err == errSkipCyclicDir || os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
fis = append(fis, fileInfoName{filename: filename, FileInfo: fi})
|
|
}
|
|
}
|
|
|
|
return fis, nil
|
|
}
|
|
|
|
func (c *capturer) newFileInfo(filename string, fi os.FileInfo, tp bundleDirType) (*fileInfo, bool) {
|
|
f := newFileInfo(c.sourceSpec, c.baseDir, filename, fi, tp)
|
|
return f, !f.disabled
|
|
}
|
|
|
|
type fileInfoName struct {
|
|
os.FileInfo
|
|
filename string
|
|
}
|
|
|
|
type bundleDirs struct {
|
|
tp bundleDirType
|
|
// Maps languages to bundles.
|
|
bundles map[string]*bundleDir
|
|
|
|
// Keeps track of language overrides for non-content files, e.g. logo.en.png.
|
|
langOverrides map[string]bool
|
|
|
|
c *capturer
|
|
}
|
|
|
|
func newBundleDirs(tp bundleDirType, c *capturer) *bundleDirs {
|
|
return &bundleDirs{tp: tp, bundles: make(map[string]*bundleDir), langOverrides: make(map[string]bool), c: c}
|
|
}
|
|
|
|
type bundleDir struct {
|
|
tp bundleDirType
|
|
fi *fileInfo
|
|
|
|
resources map[string]*fileInfo
|
|
}
|
|
|
|
func (b bundleDir) clone() *bundleDir {
|
|
b.resources = make(map[string]*fileInfo)
|
|
fic := *b.fi
|
|
b.fi = &fic
|
|
return &b
|
|
}
|
|
|
|
func newBundleDir(fi *fileInfo, bundleType bundleDirType) *bundleDir {
|
|
return &bundleDir{fi: fi, tp: bundleType, resources: make(map[string]*fileInfo)}
|
|
}
|
|
|
|
func (b *bundleDirs) addBundleContentFile(fi *fileInfo) {
|
|
dir, found := b.bundles[fi.Lang()]
|
|
if !found {
|
|
// Every bundled content file needs a bundle header.
|
|
// If one does not exist in its language, we pick the default
|
|
// language version, or a random one if that doesn't exist, either.
|
|
tl := b.c.sourceSpec.DefaultContentLanguage
|
|
ldir, found := b.bundles[tl]
|
|
if !found {
|
|
// Just pick one.
|
|
for _, v := range b.bundles {
|
|
ldir = v
|
|
break
|
|
}
|
|
}
|
|
|
|
if ldir == nil {
|
|
panic(fmt.Sprintf("bundle not found for file %q", fi.Filename()))
|
|
}
|
|
|
|
dir = ldir.clone()
|
|
dir.fi.overriddenLang = fi.Lang()
|
|
b.bundles[fi.Lang()] = dir
|
|
}
|
|
|
|
dir.resources[fi.Filename()] = fi
|
|
}
|
|
|
|
func (b *bundleDirs) addBundleFiles(fi *fileInfo) {
|
|
dir := filepath.ToSlash(fi.Dir())
|
|
p := dir + fi.TranslationBaseName() + "." + fi.Ext()
|
|
for lang, bdir := range b.bundles {
|
|
key := lang + p
|
|
// Given mypage.de.md (German translation) and mypage.md we pick the most
|
|
// the specific for that language.
|
|
if fi.Lang() == lang || !b.langOverrides[key] {
|
|
bdir.resources[key] = fi
|
|
}
|
|
b.langOverrides[key] = true
|
|
}
|
|
}
|
|
|
|
func (b *bundleDirs) addBundleHeader(fi *fileInfo) {
|
|
b.bundles[fi.Lang()] = newBundleDir(fi, b.tp)
|
|
}
|
|
|
|
func (c *capturer) isSeen(dirname string) bool {
|
|
c.seenMu.Lock()
|
|
defer c.seenMu.Unlock()
|
|
seen := c.seen[dirname]
|
|
c.seen[dirname] = true
|
|
if seen {
|
|
c.logger.WARN.Printf("Content dir %q already processed; skipped to avoid infinite recursion.", dirname)
|
|
return true
|
|
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *capturer) getRealFileInfo(path string) (os.FileInfo, string, error) {
|
|
fileInfo, err := c.lstatIfOs(path)
|
|
realPath := path
|
|
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
link, err := filepath.EvalSymlinks(path)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("Cannot read symbolic link %q, error was: %s", path, err)
|
|
}
|
|
|
|
fileInfo, err = c.lstatIfOs(link)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("Cannot stat %q, error was: %s", link, err)
|
|
}
|
|
|
|
realPath = link
|
|
|
|
if realPath != path && fileInfo.IsDir() && c.isSeen(realPath) {
|
|
// Avoid cyclic symlinks.
|
|
// Note that this may prevent some uses that isn't cyclic and also
|
|
// potential useful, but this implementation is both robust and simple:
|
|
// We stop at the first directory that we have seen before, e.g.
|
|
// /content/blog will only be processed once.
|
|
return nil, realPath, errSkipCyclicDir
|
|
}
|
|
|
|
if c.contentChanges != nil {
|
|
// Keep track of symbolic links in watch mode.
|
|
var from, to string
|
|
if fileInfo.IsDir() {
|
|
from = realPath
|
|
to = path
|
|
|
|
if !strings.HasSuffix(to, helpers.FilePathSeparator) {
|
|
to = to + helpers.FilePathSeparator
|
|
}
|
|
if !strings.HasSuffix(from, helpers.FilePathSeparator) {
|
|
from = from + helpers.FilePathSeparator
|
|
}
|
|
|
|
baseDir := c.baseDir
|
|
if !strings.HasSuffix(baseDir, helpers.FilePathSeparator) {
|
|
baseDir = baseDir + helpers.FilePathSeparator
|
|
}
|
|
|
|
if strings.HasPrefix(from, baseDir) {
|
|
// With symbolic links inside /content we need to keep
|
|
// a reference to both. This may be confusing with --navigateToChanged
|
|
// but the user has chosen this him or herself.
|
|
c.contentChanges.addSymbolicLinkMapping(from, from)
|
|
}
|
|
|
|
} else {
|
|
from = realPath
|
|
to = path
|
|
}
|
|
|
|
c.contentChanges.addSymbolicLinkMapping(from, to)
|
|
}
|
|
}
|
|
|
|
return fileInfo, realPath, nil
|
|
}
|
|
|
|
func (c *capturer) lstatIfOs(path string) (os.FileInfo, error) {
|
|
return helpers.LstatIfOs(c.fs, path)
|
|
}
|