github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/hugolib/filesystems/basefs.go (about)

     1  // Copyright 2018 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  // Package filesystems provides the fine grained file systems used by Hugo. These
    15  // are typically virtual filesystems that are composites of project and theme content.
    16  package filesystems
    17  
    18  import (
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"path"
    23  	"path/filepath"
    24  	"strings"
    25  	"sync"
    26  
    27  	"github.com/bep/overlayfs"
    28  	"github.com/gohugoio/hugo/htesting"
    29  	"github.com/gohugoio/hugo/hugofs/glob"
    30  
    31  	"github.com/gohugoio/hugo/common/herrors"
    32  	"github.com/gohugoio/hugo/common/loggers"
    33  	"github.com/gohugoio/hugo/common/types"
    34  
    35  	"github.com/rogpeppe/go-internal/lockedfile"
    36  
    37  	"github.com/gohugoio/hugo/hugofs/files"
    38  
    39  	"github.com/gohugoio/hugo/modules"
    40  
    41  	hpaths "github.com/gohugoio/hugo/common/paths"
    42  	"github.com/gohugoio/hugo/hugofs"
    43  	"github.com/gohugoio/hugo/hugolib/paths"
    44  	"github.com/spf13/afero"
    45  )
    46  
    47  const (
    48  	// Used to control concurrency between multiple Hugo instances, e.g.
    49  	// a running server and building new content with 'hugo new'.
    50  	// It's placed in the project root.
    51  	lockFileBuild = ".hugo_build.lock"
    52  )
    53  
    54  var filePathSeparator = string(filepath.Separator)
    55  
    56  // BaseFs contains the core base filesystems used by Hugo. The name "base" is used
    57  // to underline that even if they can be composites, they all have a base path set to a specific
    58  // resource folder, e.g "/my-project/content". So, no absolute filenames needed.
    59  type BaseFs struct {
    60  
    61  	// SourceFilesystems contains the different source file systems.
    62  	*SourceFilesystems
    63  
    64  	// The project source.
    65  	SourceFs afero.Fs
    66  
    67  	// The filesystem used to publish the rendered site.
    68  	// This usually maps to /my-project/public.
    69  	PublishFs afero.Fs
    70  
    71  	// The filesystem used for static files.
    72  	PublishFsStatic afero.Fs
    73  
    74  	// A read-only filesystem starting from the project workDir.
    75  	WorkDir afero.Fs
    76  
    77  	theBigFs *filesystemsCollector
    78  
    79  	workingDir string
    80  
    81  	// Locks.
    82  	buildMu Lockable // <project>/.hugo_build.lock
    83  }
    84  
    85  type Lockable interface {
    86  	Lock() (unlock func(), err error)
    87  }
    88  
    89  type fakeLockfileMutex struct {
    90  	mu sync.Mutex
    91  }
    92  
    93  func (f *fakeLockfileMutex) Lock() (func(), error) {
    94  	f.mu.Lock()
    95  	return func() { f.mu.Unlock() }, nil
    96  }
    97  
    98  // Tries to acquire a build lock.
    99  func (fs *BaseFs) LockBuild() (unlock func(), err error) {
   100  	return fs.buildMu.Lock()
   101  }
   102  
   103  // TODO(bep) we can get regular files in here and that is fine, but
   104  // we need to clean up the naming.
   105  func (fs *BaseFs) WatchDirs() []hugofs.FileMetaInfo {
   106  	var dirs []hugofs.FileMetaInfo
   107  	for _, dir := range fs.AllDirs() {
   108  		if dir.Meta().Watch {
   109  			dirs = append(dirs, dir)
   110  		}
   111  	}
   112  	return dirs
   113  }
   114  
   115  func (fs *BaseFs) AllDirs() []hugofs.FileMetaInfo {
   116  	var dirs []hugofs.FileMetaInfo
   117  	for _, dirSet := range [][]hugofs.FileMetaInfo{
   118  		fs.Archetypes.Dirs,
   119  		fs.I18n.Dirs,
   120  		fs.Data.Dirs,
   121  		fs.Content.Dirs,
   122  		fs.Assets.Dirs,
   123  		fs.Layouts.Dirs,
   124  		// fs.Resources.Dirs,
   125  		fs.StaticDirs,
   126  	} {
   127  		dirs = append(dirs, dirSet...)
   128  	}
   129  
   130  	return dirs
   131  }
   132  
   133  // RelContentDir tries to create a path relative to the content root from
   134  // the given filename. The return value is the path and language code.
   135  func (b *BaseFs) RelContentDir(filename string) string {
   136  	for _, dir := range b.SourceFilesystems.Content.Dirs {
   137  		dirname := dir.Meta().Filename
   138  		if strings.HasPrefix(filename, dirname) {
   139  			rel := path.Join(dir.Meta().Path, strings.TrimPrefix(filename, dirname))
   140  			return strings.TrimPrefix(rel, filePathSeparator)
   141  		}
   142  	}
   143  	// Either not a content dir or already relative.
   144  	return filename
   145  }
   146  
   147  // AbsProjectContentDir tries to construct a filename below the most
   148  // relevant content directory.
   149  func (b *BaseFs) AbsProjectContentDir(filename string) (string, string, error) {
   150  	isAbs := filepath.IsAbs(filename)
   151  	for _, dir := range b.SourceFilesystems.Content.Dirs {
   152  		meta := dir.Meta()
   153  		if !meta.IsProject {
   154  			continue
   155  		}
   156  
   157  		if isAbs {
   158  			if strings.HasPrefix(filename, meta.Filename) {
   159  				return strings.TrimPrefix(filename, meta.Filename), filename, nil
   160  			}
   161  		} else {
   162  			contentDir := strings.TrimPrefix(strings.TrimPrefix(meta.Filename, meta.BaseDir), filePathSeparator) + filePathSeparator
   163  
   164  			if strings.HasPrefix(filename, contentDir) {
   165  				relFilename := strings.TrimPrefix(filename, contentDir)
   166  				absFilename := filepath.Join(meta.Filename, relFilename)
   167  				return relFilename, absFilename, nil
   168  			}
   169  		}
   170  
   171  	}
   172  
   173  	if !isAbs {
   174  		// A filename on the form "posts/mypage.md", put it inside
   175  		// the first content folder, usually <workDir>/content.
   176  		// Pick the first project dir (which is probably the most important one).
   177  		for _, dir := range b.SourceFilesystems.Content.Dirs {
   178  			meta := dir.Meta()
   179  			if meta.IsProject {
   180  				return filename, filepath.Join(meta.Filename, filename), nil
   181  			}
   182  		}
   183  
   184  	}
   185  
   186  	return "", "", fmt.Errorf("could not determine content directory for %q", filename)
   187  }
   188  
   189  // ResolveJSConfigFile resolves the JS-related config file to a absolute
   190  // filename. One example of such would be postcss.config.js.
   191  func (fs *BaseFs) ResolveJSConfigFile(name string) string {
   192  	// First look in assets/_jsconfig
   193  	fi, err := fs.Assets.Fs.Stat(filepath.Join(files.FolderJSConfig, name))
   194  	if err == nil {
   195  		return fi.(hugofs.FileMetaInfo).Meta().Filename
   196  	}
   197  	// Fall back to the work dir.
   198  	fi, err = fs.Work.Stat(name)
   199  	if err == nil {
   200  		return fi.(hugofs.FileMetaInfo).Meta().Filename
   201  	}
   202  
   203  	return ""
   204  }
   205  
   206  // MakePathRelative creates a relative path from the given filename.
   207  // It returns both the component name (e.g. layouts) and the path relative to that.
   208  func (fs *BaseFs) MakePathRelative(filename string) (string, string) {
   209  	for _, sfs := range fs.FileSystems() {
   210  		if sfs.Contains(filename) {
   211  			if s, found := sfs.MakePathRelative(filename); found {
   212  				return sfs.Name, s
   213  			}
   214  		}
   215  	}
   216  	// May be a static file.
   217  	if s := fs.MakeStaticPathRelative(filename); s != "" {
   218  		return files.ComponentFolderStatic, s
   219  	}
   220  	// Fall back to relative to the working dir.
   221  	if strings.HasPrefix(filename, fs.workingDir) {
   222  		return "", strings.TrimPrefix(filename, fs.workingDir)
   223  	}
   224  	return "", ""
   225  }
   226  
   227  // SourceFilesystems contains the different source file systems. These can be
   228  // composite file systems (theme and project etc.), and they have all root
   229  // set to the source type the provides: data, i18n, static, layouts.
   230  type SourceFilesystems struct {
   231  	Content    *SourceFilesystem
   232  	Data       *SourceFilesystem
   233  	I18n       *SourceFilesystem
   234  	Layouts    *SourceFilesystem
   235  	Archetypes *SourceFilesystem
   236  	Assets     *SourceFilesystem
   237  
   238  	// Writable filesystem on top the project's resources directory,
   239  	// with any sub module's resource fs layered below.
   240  	ResourcesCache afero.Fs
   241  
   242  	// The work folder (may be a composite of project and theme components).
   243  	Work afero.Fs
   244  
   245  	// When in multihost we have one static filesystem per language. The sync
   246  	// static files is currently done outside of the Hugo build (where there is
   247  	// a concept of a site per language).
   248  	// When in non-multihost mode there will be one entry in this map with a blank key.
   249  	Static map[string]*SourceFilesystem
   250  
   251  	// All the /static dirs (including themes/modules).
   252  	StaticDirs []hugofs.FileMetaInfo
   253  }
   254  
   255  // FileSystems returns the FileSystems relevant for the change detection
   256  // in server mode.
   257  // Note: This does currently not return any static fs.
   258  func (s *SourceFilesystems) FileSystems() []*SourceFilesystem {
   259  	return []*SourceFilesystem{
   260  		s.Content,
   261  		s.Assets,
   262  		s.Data,
   263  		s.I18n,
   264  		s.Layouts,
   265  		s.Archetypes,
   266  		// TODO(bep) static
   267  	}
   268  }
   269  
   270  // A SourceFilesystem holds the filesystem for a given source type in Hugo (data,
   271  // i18n, layouts, static) and additional metadata to be able to use that filesystem
   272  // in server mode.
   273  type SourceFilesystem struct {
   274  	// Name matches one in files.ComponentFolders
   275  	Name string
   276  
   277  	// This is a virtual composite filesystem. It expects path relative to a context.
   278  	Fs afero.Fs
   279  
   280  	// This filesystem as separate root directories, starting from project and down
   281  	// to the themes/modules.
   282  	Dirs []hugofs.FileMetaInfo
   283  
   284  	// When syncing a source folder to the target (e.g. /public), this may
   285  	// be set to publish into a subfolder. This is used for static syncing
   286  	// in multihost mode.
   287  	PublishFolder string
   288  }
   289  
   290  // ContentStaticAssetFs will create a new composite filesystem from the content,
   291  // static, and asset filesystems. The site language is needed to pick the correct static filesystem.
   292  // The order is content, static and then assets.
   293  // TODO(bep) check usage
   294  func (s SourceFilesystems) ContentStaticAssetFs(lang string) afero.Fs {
   295  	return overlayfs.New(
   296  		overlayfs.Options{
   297  			Fss: []afero.Fs{
   298  				s.Content.Fs,
   299  				s.StaticFs(lang),
   300  				s.Assets.Fs,
   301  			},
   302  		},
   303  	)
   304  
   305  }
   306  
   307  // StaticFs returns the static filesystem for the given language.
   308  // This can be a composite filesystem.
   309  func (s SourceFilesystems) StaticFs(lang string) afero.Fs {
   310  	var staticFs afero.Fs = hugofs.NoOpFs
   311  
   312  	if fs, ok := s.Static[lang]; ok {
   313  		staticFs = fs.Fs
   314  	} else if fs, ok := s.Static[""]; ok {
   315  		staticFs = fs.Fs
   316  	}
   317  
   318  	return staticFs
   319  }
   320  
   321  // StatResource looks for a resource in these filesystems in order: static, assets and finally content.
   322  // If found in any of them, it returns FileInfo and the relevant filesystem.
   323  // Any non herrors.IsNotExist error will be returned.
   324  // An herrors.IsNotExist error will be returned only if all filesystems return such an error.
   325  // Note that if we only wanted to find the file, we could create a composite Afero fs,
   326  // but we also need to know which filesystem root it lives in.
   327  func (s SourceFilesystems) StatResource(lang, filename string) (fi os.FileInfo, fs afero.Fs, err error) {
   328  	for _, fsToCheck := range []afero.Fs{s.StaticFs(lang), s.Assets.Fs, s.Content.Fs} {
   329  		fs = fsToCheck
   330  		fi, err = fs.Stat(filename)
   331  		if err == nil || !herrors.IsNotExist(err) {
   332  			return
   333  		}
   334  	}
   335  	// Not found.
   336  	return
   337  }
   338  
   339  // IsStatic returns true if the given filename is a member of one of the static
   340  // filesystems.
   341  func (s SourceFilesystems) IsStatic(filename string) bool {
   342  	for _, staticFs := range s.Static {
   343  		if staticFs.Contains(filename) {
   344  			return true
   345  		}
   346  	}
   347  	return false
   348  }
   349  
   350  // IsContent returns true if the given filename is a member of the content filesystem.
   351  func (s SourceFilesystems) IsContent(filename string) bool {
   352  	return s.Content.Contains(filename)
   353  }
   354  
   355  // IsLayout returns true if the given filename is a member of the layouts filesystem.
   356  func (s SourceFilesystems) IsLayout(filename string) bool {
   357  	return s.Layouts.Contains(filename)
   358  }
   359  
   360  // IsData returns true if the given filename is a member of the data filesystem.
   361  func (s SourceFilesystems) IsData(filename string) bool {
   362  	return s.Data.Contains(filename)
   363  }
   364  
   365  // IsAsset returns true if the given filename is a member of the asset filesystem.
   366  func (s SourceFilesystems) IsAsset(filename string) bool {
   367  	return s.Assets.Contains(filename)
   368  }
   369  
   370  // IsI18n returns true if the given filename is a member of the i18n filesystem.
   371  func (s SourceFilesystems) IsI18n(filename string) bool {
   372  	return s.I18n.Contains(filename)
   373  }
   374  
   375  // MakeStaticPathRelative makes an absolute static filename into a relative one.
   376  // It will return an empty string if the filename is not a member of a static filesystem.
   377  func (s SourceFilesystems) MakeStaticPathRelative(filename string) string {
   378  	for _, staticFs := range s.Static {
   379  		rel, _ := staticFs.MakePathRelative(filename)
   380  		if rel != "" {
   381  			return rel
   382  		}
   383  	}
   384  	return ""
   385  }
   386  
   387  // MakePathRelative creates a relative path from the given filename.
   388  func (d *SourceFilesystem) MakePathRelative(filename string) (string, bool) {
   389  	for _, dir := range d.Dirs {
   390  		meta := dir.(hugofs.FileMetaInfo).Meta()
   391  		currentPath := meta.Filename
   392  
   393  		if strings.HasPrefix(filename, currentPath) {
   394  			rel := strings.TrimPrefix(filename, currentPath)
   395  			if mp := meta.Path; mp != "" {
   396  				rel = filepath.Join(mp, rel)
   397  			}
   398  			return strings.TrimPrefix(rel, filePathSeparator), true
   399  		}
   400  	}
   401  	return "", false
   402  }
   403  
   404  func (d *SourceFilesystem) RealFilename(rel string) string {
   405  	fi, err := d.Fs.Stat(rel)
   406  	if err != nil {
   407  		return rel
   408  	}
   409  	if realfi, ok := fi.(hugofs.FileMetaInfo); ok {
   410  		return realfi.Meta().Filename
   411  	}
   412  
   413  	return rel
   414  }
   415  
   416  // Contains returns whether the given filename is a member of the current filesystem.
   417  func (d *SourceFilesystem) Contains(filename string) bool {
   418  	for _, dir := range d.Dirs {
   419  		if strings.HasPrefix(filename, dir.Meta().Filename) {
   420  			return true
   421  		}
   422  	}
   423  	return false
   424  }
   425  
   426  // Path returns the mount relative path to the given filename if it is a member of
   427  // of the current filesystem, an empty string if not.
   428  func (d *SourceFilesystem) Path(filename string) string {
   429  	for _, dir := range d.Dirs {
   430  		meta := dir.Meta()
   431  		if strings.HasPrefix(filename, meta.Filename) {
   432  			p := strings.TrimPrefix(strings.TrimPrefix(filename, meta.Filename), filePathSeparator)
   433  			if mountRoot := meta.MountRoot; mountRoot != "" {
   434  				return filepath.Join(mountRoot, p)
   435  			}
   436  			return p
   437  		}
   438  	}
   439  	return ""
   440  }
   441  
   442  // RealDirs gets a list of absolute paths to directories starting from the given
   443  // path.
   444  func (d *SourceFilesystem) RealDirs(from string) []string {
   445  	var dirnames []string
   446  	for _, dir := range d.Dirs {
   447  		meta := dir.Meta()
   448  		dirname := filepath.Join(meta.Filename, from)
   449  		_, err := meta.Fs.Stat(from)
   450  
   451  		if err == nil {
   452  			dirnames = append(dirnames, dirname)
   453  		}
   454  	}
   455  	return dirnames
   456  }
   457  
   458  // WithBaseFs allows reuse of some potentially expensive to create parts that remain
   459  // the same across sites/languages.
   460  func WithBaseFs(b *BaseFs) func(*BaseFs) error {
   461  	return func(bb *BaseFs) error {
   462  		bb.theBigFs = b.theBigFs
   463  		bb.SourceFilesystems = b.SourceFilesystems
   464  		return nil
   465  	}
   466  }
   467  
   468  var counter int
   469  
   470  // NewBase builds the filesystems used by Hugo given the paths and options provided.NewBase
   471  func NewBase(p *paths.Paths, logger loggers.Logger, options ...func(*BaseFs) error) (*BaseFs, error) {
   472  	fs := p.Fs
   473  	if logger == nil {
   474  		logger = loggers.NewDefault()
   475  	}
   476  
   477  	publishFs := hugofs.NewBaseFileDecorator(fs.PublishDir)
   478  	sourceFs := hugofs.NewBaseFileDecorator(afero.NewBasePathFs(fs.Source, p.Cfg.BaseConfig().WorkingDir))
   479  	publishFsStatic := fs.PublishDirStatic
   480  
   481  	var buildMu Lockable
   482  	if p.Cfg.NoBuildLock() || htesting.IsTest {
   483  		buildMu = &fakeLockfileMutex{}
   484  	} else {
   485  		buildMu = lockedfile.MutexAt(filepath.Join(p.Cfg.BaseConfig().WorkingDir, lockFileBuild))
   486  	}
   487  
   488  	b := &BaseFs{
   489  		SourceFs:        sourceFs,
   490  		WorkDir:         fs.WorkingDirReadOnly,
   491  		PublishFs:       publishFs,
   492  		PublishFsStatic: publishFsStatic,
   493  		workingDir:      p.Cfg.BaseConfig().WorkingDir,
   494  		buildMu:         buildMu,
   495  	}
   496  
   497  	for _, opt := range options {
   498  		if err := opt(b); err != nil {
   499  			return nil, err
   500  		}
   501  	}
   502  
   503  	if b.theBigFs != nil && b.SourceFilesystems != nil {
   504  		return b, nil
   505  	}
   506  
   507  	builder := newSourceFilesystemsBuilder(p, logger, b)
   508  	sourceFilesystems, err := builder.Build()
   509  	if err != nil {
   510  		return nil, fmt.Errorf("build filesystems: %w", err)
   511  	}
   512  
   513  	b.SourceFilesystems = sourceFilesystems
   514  	b.theBigFs = builder.theBigFs
   515  
   516  	return b, nil
   517  }
   518  
   519  type sourceFilesystemsBuilder struct {
   520  	logger   loggers.Logger
   521  	p        *paths.Paths
   522  	sourceFs afero.Fs
   523  	result   *SourceFilesystems
   524  	theBigFs *filesystemsCollector
   525  }
   526  
   527  func newSourceFilesystemsBuilder(p *paths.Paths, logger loggers.Logger, b *BaseFs) *sourceFilesystemsBuilder {
   528  	sourceFs := hugofs.NewBaseFileDecorator(p.Fs.Source)
   529  	return &sourceFilesystemsBuilder{p: p, logger: logger, sourceFs: sourceFs, theBigFs: b.theBigFs, result: &SourceFilesystems{}}
   530  }
   531  
   532  func (b *sourceFilesystemsBuilder) newSourceFilesystem(name string, fs afero.Fs, dirs []hugofs.FileMetaInfo) *SourceFilesystem {
   533  	return &SourceFilesystem{
   534  		Name: name,
   535  		Fs:   fs,
   536  		Dirs: dirs,
   537  	}
   538  }
   539  
   540  func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
   541  	if b.theBigFs == nil {
   542  		theBigFs, err := b.createMainOverlayFs(b.p)
   543  		if err != nil {
   544  			return nil, fmt.Errorf("create main fs: %w", err)
   545  		}
   546  
   547  		b.theBigFs = theBigFs
   548  	}
   549  
   550  	createView := func(componentID string) *SourceFilesystem {
   551  		if b.theBigFs == nil || b.theBigFs.overlayMounts == nil {
   552  			return b.newSourceFilesystem(componentID, hugofs.NoOpFs, nil)
   553  		}
   554  
   555  		dirs := b.theBigFs.overlayDirs[componentID]
   556  
   557  		return b.newSourceFilesystem(componentID, afero.NewBasePathFs(b.theBigFs.overlayMounts, componentID), dirs)
   558  	}
   559  
   560  	b.result.Archetypes = createView(files.ComponentFolderArchetypes)
   561  	b.result.Layouts = createView(files.ComponentFolderLayouts)
   562  	b.result.Assets = createView(files.ComponentFolderAssets)
   563  	b.result.ResourcesCache = b.theBigFs.overlayResources
   564  
   565  	// Data, i18n and content cannot use the overlay fs
   566  	dataDirs := b.theBigFs.overlayDirs[files.ComponentFolderData]
   567  	dataFs, err := hugofs.NewSliceFs(dataDirs...)
   568  	if err != nil {
   569  		return nil, err
   570  	}
   571  
   572  	b.result.Data = b.newSourceFilesystem(files.ComponentFolderData, dataFs, dataDirs)
   573  
   574  	i18nDirs := b.theBigFs.overlayDirs[files.ComponentFolderI18n]
   575  	i18nFs, err := hugofs.NewSliceFs(i18nDirs...)
   576  	if err != nil {
   577  		return nil, err
   578  	}
   579  	b.result.I18n = b.newSourceFilesystem(files.ComponentFolderI18n, i18nFs, i18nDirs)
   580  
   581  	contentDirs := b.theBigFs.overlayDirs[files.ComponentFolderContent]
   582  	contentBfs := afero.NewBasePathFs(b.theBigFs.overlayMountsContent, files.ComponentFolderContent)
   583  
   584  	contentFs, err := hugofs.NewLanguageFs(b.p.Cfg.LanguagesDefaultFirst().AsOrdinalSet(), contentBfs)
   585  	if err != nil {
   586  		return nil, fmt.Errorf("create content filesystem: %w", err)
   587  	}
   588  
   589  	b.result.Content = b.newSourceFilesystem(files.ComponentFolderContent, contentFs, contentDirs)
   590  
   591  	b.result.Work = afero.NewReadOnlyFs(b.theBigFs.overlayFull)
   592  
   593  	// Create static filesystem(s)
   594  	ms := make(map[string]*SourceFilesystem)
   595  	b.result.Static = ms
   596  	b.result.StaticDirs = b.theBigFs.overlayDirs[files.ComponentFolderStatic]
   597  
   598  	if b.theBigFs.staticPerLanguage != nil {
   599  		// Multihost mode
   600  		for k, v := range b.theBigFs.staticPerLanguage {
   601  			sfs := b.newSourceFilesystem(files.ComponentFolderStatic, v, b.result.StaticDirs)
   602  			sfs.PublishFolder = k
   603  			ms[k] = sfs
   604  		}
   605  	} else {
   606  		bfs := afero.NewBasePathFs(b.theBigFs.overlayMountsStatic, files.ComponentFolderStatic)
   607  		ms[""] = b.newSourceFilesystem(files.ComponentFolderStatic, bfs, b.result.StaticDirs)
   608  	}
   609  
   610  	return b.result, nil
   611  }
   612  
   613  func (b *sourceFilesystemsBuilder) createMainOverlayFs(p *paths.Paths) (*filesystemsCollector, error) {
   614  	var staticFsMap map[string]*overlayfs.OverlayFs
   615  	if b.p.Cfg.IsMultihost() {
   616  		languages := b.p.Cfg.Languages()
   617  		staticFsMap = make(map[string]*overlayfs.OverlayFs)
   618  		for _, l := range languages {
   619  			staticFsMap[l.Lang] = overlayfs.New(overlayfs.Options{})
   620  		}
   621  	}
   622  
   623  	collector := &filesystemsCollector{
   624  		sourceProject:     b.sourceFs,
   625  		sourceModules:     hugofs.NewNoSymlinkFs(b.sourceFs, b.logger, false),
   626  		overlayDirs:       make(map[string][]hugofs.FileMetaInfo),
   627  		staticPerLanguage: staticFsMap,
   628  
   629  		overlayMounts:        overlayfs.New(overlayfs.Options{}),
   630  		overlayMountsContent: overlayfs.New(overlayfs.Options{DirsMerger: hugofs.LanguageDirsMerger}),
   631  		overlayMountsStatic:  overlayfs.New(overlayfs.Options{DirsMerger: hugofs.LanguageDirsMerger}),
   632  		overlayFull:          overlayfs.New(overlayfs.Options{}),
   633  		overlayResources:     overlayfs.New(overlayfs.Options{FirstWritable: true}),
   634  	}
   635  
   636  	mods := p.AllModules()
   637  
   638  	mounts := make([]mountsDescriptor, len(mods))
   639  
   640  	for i := 0; i < len(mods); i++ {
   641  		mod := mods[i]
   642  		dir := mod.Dir()
   643  
   644  		isMainProject := mod.Owner() == nil
   645  		mounts[i] = mountsDescriptor{
   646  			Module:        mod,
   647  			dir:           dir,
   648  			isMainProject: isMainProject,
   649  			ordinal:       i,
   650  		}
   651  
   652  	}
   653  
   654  	err := b.createOverlayFs(collector, mounts)
   655  
   656  	return collector, err
   657  }
   658  
   659  func (b *sourceFilesystemsBuilder) isContentMount(mnt modules.Mount) bool {
   660  	return strings.HasPrefix(mnt.Target, files.ComponentFolderContent)
   661  }
   662  
   663  func (b *sourceFilesystemsBuilder) isStaticMount(mnt modules.Mount) bool {
   664  	return strings.HasPrefix(mnt.Target, files.ComponentFolderStatic)
   665  }
   666  
   667  func (b *sourceFilesystemsBuilder) createOverlayFs(
   668  	collector *filesystemsCollector,
   669  	mounts []mountsDescriptor) error {
   670  
   671  	if len(mounts) == 0 {
   672  		appendNopIfEmpty := func(ofs *overlayfs.OverlayFs) *overlayfs.OverlayFs {
   673  			if ofs.NumFilesystems() > 0 {
   674  				return ofs
   675  			}
   676  			return ofs.Append(hugofs.NoOpFs)
   677  		}
   678  		collector.overlayMounts = appendNopIfEmpty(collector.overlayMounts)
   679  		collector.overlayMountsContent = appendNopIfEmpty(collector.overlayMountsContent)
   680  		collector.overlayMountsStatic = appendNopIfEmpty(collector.overlayMountsStatic)
   681  		collector.overlayFull = appendNopIfEmpty(collector.overlayFull)
   682  		collector.overlayResources = appendNopIfEmpty(collector.overlayResources)
   683  
   684  		return nil
   685  	}
   686  
   687  	for _, md := range mounts {
   688  		var (
   689  			fromTo        []hugofs.RootMapping
   690  			fromToContent []hugofs.RootMapping
   691  			fromToStatic  []hugofs.RootMapping
   692  		)
   693  
   694  		absPathify := func(path string) (string, string) {
   695  			if filepath.IsAbs(path) {
   696  				return "", path
   697  			}
   698  			return md.dir, hpaths.AbsPathify(md.dir, path)
   699  		}
   700  
   701  		for i, mount := range md.Mounts() {
   702  			// Add more weight to early mounts.
   703  			// When two mounts contain the same filename,
   704  			// the first entry wins.
   705  			mountWeight := (10 + md.ordinal) * (len(md.Mounts()) - i)
   706  
   707  			inclusionFilter, err := glob.NewFilenameFilter(
   708  				types.ToStringSlicePreserveString(mount.IncludeFiles),
   709  				types.ToStringSlicePreserveString(mount.ExcludeFiles),
   710  			)
   711  			if err != nil {
   712  				return err
   713  			}
   714  
   715  			base, filename := absPathify(mount.Source)
   716  
   717  			rm := hugofs.RootMapping{
   718  				From:      mount.Target,
   719  				To:        filename,
   720  				ToBasedir: base,
   721  				Module:    md.Module.Path(),
   722  				IsProject: md.isMainProject,
   723  				Meta: &hugofs.FileMeta{
   724  					Watch:           md.Watch(),
   725  					Weight:          mountWeight,
   726  					Classifier:      files.ContentClassContent,
   727  					InclusionFilter: inclusionFilter,
   728  				},
   729  			}
   730  
   731  			isContentMount := b.isContentMount(mount)
   732  
   733  			lang := mount.Lang
   734  			if lang == "" && isContentMount {
   735  				lang = b.p.Cfg.DefaultContentLanguage()
   736  			}
   737  
   738  			rm.Meta.Lang = lang
   739  
   740  			if isContentMount {
   741  				fromToContent = append(fromToContent, rm)
   742  			} else if b.isStaticMount(mount) {
   743  				fromToStatic = append(fromToStatic, rm)
   744  			} else {
   745  				fromTo = append(fromTo, rm)
   746  			}
   747  		}
   748  
   749  		modBase := collector.sourceProject
   750  		if !md.isMainProject {
   751  			modBase = collector.sourceModules
   752  		}
   753  		sourceStatic := hugofs.NewNoSymlinkFs(modBase, b.logger, true)
   754  
   755  		rmfs, err := hugofs.NewRootMappingFs(modBase, fromTo...)
   756  		if err != nil {
   757  			return err
   758  		}
   759  		rmfsContent, err := hugofs.NewRootMappingFs(modBase, fromToContent...)
   760  		if err != nil {
   761  			return err
   762  		}
   763  		rmfsStatic, err := hugofs.NewRootMappingFs(sourceStatic, fromToStatic...)
   764  		if err != nil {
   765  			return err
   766  		}
   767  
   768  		// We need to keep the ordered list of directories for watching and
   769  		// some special merge operations (data, i18n).
   770  		collector.addDirs(rmfs)
   771  		collector.addDirs(rmfsContent)
   772  		collector.addDirs(rmfsStatic)
   773  
   774  		if collector.staticPerLanguage != nil {
   775  			for _, l := range b.p.Cfg.Languages() {
   776  				lang := l.Lang
   777  
   778  				lfs := rmfsStatic.Filter(func(rm hugofs.RootMapping) bool {
   779  					rlang := rm.Meta.Lang
   780  					return rlang == "" || rlang == lang
   781  				})
   782  				bfs := afero.NewBasePathFs(lfs, files.ComponentFolderStatic)
   783  				collector.staticPerLanguage[lang] = collector.staticPerLanguage[lang].Append(bfs)
   784  			}
   785  		}
   786  
   787  		getResourcesDir := func() string {
   788  			if md.isMainProject {
   789  				return b.p.AbsResourcesDir
   790  			}
   791  			_, filename := absPathify(files.FolderResources)
   792  			return filename
   793  		}
   794  
   795  		collector.overlayMounts = collector.overlayMounts.Append(rmfs)
   796  		collector.overlayMountsContent = collector.overlayMountsContent.Append(rmfsContent)
   797  		collector.overlayMountsStatic = collector.overlayMountsStatic.Append(rmfsStatic)
   798  		collector.overlayFull = collector.overlayFull.Append(afero.NewBasePathFs(modBase, md.dir))
   799  		collector.overlayResources = collector.overlayResources.Append(afero.NewBasePathFs(modBase, getResourcesDir()))
   800  
   801  	}
   802  
   803  	return nil
   804  }
   805  
   806  func printFs(fs afero.Fs, path string, w io.Writer) {
   807  	if fs == nil {
   808  		return
   809  	}
   810  	afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error {
   811  		if err != nil {
   812  			return err
   813  		}
   814  		if info.IsDir() {
   815  			return nil
   816  		}
   817  		var filename string
   818  		if fim, ok := info.(hugofs.FileMetaInfo); ok {
   819  			filename = fim.Meta().Filename
   820  		}
   821  		fmt.Fprintf(w, "    %q %q\n", path, filename)
   822  		return nil
   823  	})
   824  }
   825  
   826  type filesystemsCollector struct {
   827  	sourceProject afero.Fs // Source for project folders
   828  	sourceModules afero.Fs // Source for modules/themes
   829  
   830  	overlayMounts        *overlayfs.OverlayFs
   831  	overlayMountsContent *overlayfs.OverlayFs
   832  	overlayMountsStatic  *overlayfs.OverlayFs
   833  	overlayFull          *overlayfs.OverlayFs
   834  	overlayResources     *overlayfs.OverlayFs
   835  
   836  	// Maps component type (layouts, static, content etc.) an ordered list of
   837  	// directories representing the overlay filesystems above.
   838  	overlayDirs map[string][]hugofs.FileMetaInfo
   839  
   840  	// Set if in multihost mode
   841  	staticPerLanguage map[string]*overlayfs.OverlayFs
   842  
   843  	finalizerInit sync.Once
   844  }
   845  
   846  func (c *filesystemsCollector) addDirs(rfs *hugofs.RootMappingFs) {
   847  	for _, componentFolder := range files.ComponentFolders {
   848  		c.addDir(rfs, componentFolder)
   849  	}
   850  }
   851  
   852  func (c *filesystemsCollector) addDir(rfs *hugofs.RootMappingFs, componentFolder string) {
   853  	dirs, err := rfs.Dirs(componentFolder)
   854  
   855  	if err == nil {
   856  		c.overlayDirs[componentFolder] = append(c.overlayDirs[componentFolder], dirs...)
   857  	}
   858  }
   859  
   860  func (c *filesystemsCollector) reverseFis(fis []hugofs.FileMetaInfo) {
   861  	for i := len(fis)/2 - 1; i >= 0; i-- {
   862  		opp := len(fis) - 1 - i
   863  		fis[i], fis[opp] = fis[opp], fis[i]
   864  	}
   865  }
   866  
   867  type mountsDescriptor struct {
   868  	modules.Module
   869  	dir           string
   870  	isMainProject bool
   871  	ordinal       int
   872  }