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