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