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