github.com/pietrocarrara/hugo@v0.47.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  	"errors"
    20  	"io"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  
    25  	"github.com/gohugoio/hugo/config"
    26  
    27  	"github.com/gohugoio/hugo/hugofs"
    28  
    29  	"fmt"
    30  
    31  	"github.com/gohugoio/hugo/hugolib/paths"
    32  	"github.com/gohugoio/hugo/langs"
    33  	"github.com/spf13/afero"
    34  )
    35  
    36  // When we create a virtual filesystem with data and i18n bundles for the project and the themes,
    37  // this is the name of the project's virtual root. It got it's funky name to make sure
    38  // (or very unlikely) that it collides with a theme name.
    39  const projectVirtualFolder = "__h__project"
    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 filesystem used to publish the rendered site.
    52  	// This usually maps to /my-project/public.
    53  	PublishFs afero.Fs
    54  
    55  	themeFs afero.Fs
    56  
    57  	// TODO(bep) improve the "theme interaction"
    58  	AbsThemeDirs []string
    59  }
    60  
    61  // RelContentDir tries to create a path relative to the content root from
    62  // the given filename. The return value is the path and language code.
    63  func (b *BaseFs) RelContentDir(filename string) string {
    64  	for _, dirname := range b.SourceFilesystems.Content.Dirnames {
    65  		if strings.HasPrefix(filename, dirname) {
    66  			rel := strings.TrimPrefix(filename, dirname)
    67  			return strings.TrimPrefix(rel, filePathSeparator)
    68  		}
    69  	}
    70  	// Either not a content dir or already relative.
    71  	return filename
    72  }
    73  
    74  // SourceFilesystems contains the different source file systems. These can be
    75  // composite file systems (theme and project etc.), and they have all root
    76  // set to the source type the provides: data, i18n, static, layouts.
    77  type SourceFilesystems struct {
    78  	Content    *SourceFilesystem
    79  	Data       *SourceFilesystem
    80  	I18n       *SourceFilesystem
    81  	Layouts    *SourceFilesystem
    82  	Archetypes *SourceFilesystem
    83  	Assets     *SourceFilesystem
    84  	Resources  *SourceFilesystem
    85  
    86  	// This is a unified read-only view of the project's and themes' workdir.
    87  	Work *SourceFilesystem
    88  
    89  	// When in multihost we have one static filesystem per language. The sync
    90  	// static files is currently done outside of the Hugo build (where there is
    91  	// a concept of a site per language).
    92  	// When in non-multihost mode there will be one entry in this map with a blank key.
    93  	Static map[string]*SourceFilesystem
    94  }
    95  
    96  // A SourceFilesystem holds the filesystem for a given source type in Hugo (data,
    97  // i18n, layouts, static) and additional metadata to be able to use that filesystem
    98  // in server mode.
    99  type SourceFilesystem struct {
   100  	// This is a virtual composite filesystem. It expects path relative to a context.
   101  	Fs afero.Fs
   102  
   103  	// This is the base source filesystem. In real Hugo, this will be the OS filesystem.
   104  	// Use this if you need to resolve items in Dirnames below.
   105  	SourceFs afero.Fs
   106  
   107  	// Dirnames is absolute filenames to the directories in this filesystem.
   108  	Dirnames []string
   109  
   110  	// When syncing a source folder to the target (e.g. /public), this may
   111  	// be set to publish into a subfolder. This is used for static syncing
   112  	// in multihost mode.
   113  	PublishFolder string
   114  }
   115  
   116  // ContentStaticAssetFs will create a new composite filesystem from the content,
   117  // static, and asset filesystems. The site language is needed to pick the correct static filesystem.
   118  // The order is content, static and then assets.
   119  // TODO(bep) check usage
   120  func (s SourceFilesystems) ContentStaticAssetFs(lang string) afero.Fs {
   121  	staticFs := s.StaticFs(lang)
   122  
   123  	base := afero.NewCopyOnWriteFs(s.Assets.Fs, staticFs)
   124  	return afero.NewCopyOnWriteFs(base, s.Content.Fs)
   125  
   126  }
   127  
   128  // StaticFs returns the static filesystem for the given language.
   129  // This can be a composite filesystem.
   130  func (s SourceFilesystems) StaticFs(lang string) afero.Fs {
   131  	var staticFs afero.Fs = hugofs.NoOpFs
   132  
   133  	if fs, ok := s.Static[lang]; ok {
   134  		staticFs = fs.Fs
   135  	} else if fs, ok := s.Static[""]; ok {
   136  		staticFs = fs.Fs
   137  	}
   138  
   139  	return staticFs
   140  }
   141  
   142  // StatResource looks for a resource in these filesystems in order: static, assets and finally content.
   143  // If found in any of them, it returns FileInfo and the relevant filesystem.
   144  // Any non os.IsNotExist error will be returned.
   145  // An os.IsNotExist error wil be returned only if all filesystems return such an error.
   146  // Note that if we only wanted to find the file, we could create a composite Afero fs,
   147  // but we also need to know which filesystem root it lives in.
   148  func (s SourceFilesystems) StatResource(lang, filename string) (fi os.FileInfo, fs afero.Fs, err error) {
   149  	for _, fsToCheck := range []afero.Fs{s.StaticFs(lang), s.Assets.Fs, s.Content.Fs} {
   150  		fs = fsToCheck
   151  		fi, err = fs.Stat(filename)
   152  		if err == nil || !os.IsNotExist(err) {
   153  			return
   154  		}
   155  	}
   156  	// Not found.
   157  	return
   158  }
   159  
   160  // IsStatic returns true if the given filename is a member of one of the static
   161  // filesystems.
   162  func (s SourceFilesystems) IsStatic(filename string) bool {
   163  	for _, staticFs := range s.Static {
   164  		if staticFs.Contains(filename) {
   165  			return true
   166  		}
   167  	}
   168  	return false
   169  }
   170  
   171  // IsContent returns true if the given filename is a member of the content filesystem.
   172  func (s SourceFilesystems) IsContent(filename string) bool {
   173  	return s.Content.Contains(filename)
   174  }
   175  
   176  // IsLayout returns true if the given filename is a member of the layouts filesystem.
   177  func (s SourceFilesystems) IsLayout(filename string) bool {
   178  	return s.Layouts.Contains(filename)
   179  }
   180  
   181  // IsData returns true if the given filename is a member of the data filesystem.
   182  func (s SourceFilesystems) IsData(filename string) bool {
   183  	return s.Data.Contains(filename)
   184  }
   185  
   186  // IsAsset returns true if the given filename is a member of the data filesystem.
   187  func (s SourceFilesystems) IsAsset(filename string) bool {
   188  	return s.Assets.Contains(filename)
   189  }
   190  
   191  // IsI18n returns true if the given filename is a member of the i18n filesystem.
   192  func (s SourceFilesystems) IsI18n(filename string) bool {
   193  	return s.I18n.Contains(filename)
   194  }
   195  
   196  // MakeStaticPathRelative makes an absolute static filename into a relative one.
   197  // It will return an empty string if the filename is not a member of a static filesystem.
   198  func (s SourceFilesystems) MakeStaticPathRelative(filename string) string {
   199  	for _, staticFs := range s.Static {
   200  		rel := staticFs.MakePathRelative(filename)
   201  		if rel != "" {
   202  			return rel
   203  		}
   204  	}
   205  	return ""
   206  }
   207  
   208  // MakePathRelative creates a relative path from the given filename.
   209  // It will return an empty string if the filename is not a member of this filesystem.
   210  func (d *SourceFilesystem) MakePathRelative(filename string) string {
   211  	for _, currentPath := range d.Dirnames {
   212  		if strings.HasPrefix(filename, currentPath) {
   213  			return strings.TrimPrefix(filename, currentPath)
   214  		}
   215  	}
   216  	return ""
   217  }
   218  
   219  func (d *SourceFilesystem) RealFilename(rel string) string {
   220  	fi, err := d.Fs.Stat(rel)
   221  	if err != nil {
   222  		return rel
   223  	}
   224  	if realfi, ok := fi.(hugofs.RealFilenameInfo); ok {
   225  		return realfi.RealFilename()
   226  	}
   227  
   228  	return rel
   229  }
   230  
   231  // Contains returns whether the given filename is a member of the current filesystem.
   232  func (d *SourceFilesystem) Contains(filename string) bool {
   233  	for _, dir := range d.Dirnames {
   234  		if strings.HasPrefix(filename, dir) {
   235  			return true
   236  		}
   237  	}
   238  	return false
   239  }
   240  
   241  // RealDirs gets a list of absolute paths to directories starting from the given
   242  // path.
   243  func (d *SourceFilesystem) RealDirs(from string) []string {
   244  	var dirnames []string
   245  	for _, dir := range d.Dirnames {
   246  		dirname := filepath.Join(dir, from)
   247  		if _, err := d.SourceFs.Stat(dirname); err == nil {
   248  			dirnames = append(dirnames, dirname)
   249  		}
   250  	}
   251  	return dirnames
   252  }
   253  
   254  // WithBaseFs allows reuse of some potentially expensive to create parts that remain
   255  // the same across sites/languages.
   256  func WithBaseFs(b *BaseFs) func(*BaseFs) error {
   257  	return func(bb *BaseFs) error {
   258  		bb.themeFs = b.themeFs
   259  		bb.AbsThemeDirs = b.AbsThemeDirs
   260  		return nil
   261  	}
   262  }
   263  
   264  func newRealBase(base afero.Fs) afero.Fs {
   265  	return hugofs.NewBasePathRealFilenameFs(base.(*afero.BasePathFs))
   266  
   267  }
   268  
   269  // NewBase builds the filesystems used by Hugo given the paths and options provided.NewBase
   270  func NewBase(p *paths.Paths, options ...func(*BaseFs) error) (*BaseFs, error) {
   271  	fs := p.Fs
   272  
   273  	publishFs := afero.NewBasePathFs(fs.Destination, p.AbsPublishDir)
   274  
   275  	contentFs, absContentDirs, err := createContentFs(fs.Source, p.WorkingDir, p.DefaultContentLanguage, p.Languages)
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  
   280  	// Make sure we don't have any overlapping content dirs. That will never work.
   281  	for i, d1 := range absContentDirs {
   282  		for j, d2 := range absContentDirs {
   283  			if i == j {
   284  				continue
   285  			}
   286  			if strings.HasPrefix(d1, d2) || strings.HasPrefix(d2, d1) {
   287  				return nil, fmt.Errorf("found overlapping content dirs (%q and %q)", d1, d2)
   288  			}
   289  		}
   290  	}
   291  
   292  	b := &BaseFs{
   293  		PublishFs: publishFs,
   294  	}
   295  
   296  	for _, opt := range options {
   297  		if err := opt(b); err != nil {
   298  			return nil, err
   299  		}
   300  	}
   301  
   302  	builder := newSourceFilesystemsBuilder(p, b)
   303  	sourceFilesystems, err := builder.Build()
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  
   308  	sourceFilesystems.Content = &SourceFilesystem{
   309  		SourceFs: fs.Source,
   310  		Fs:       contentFs,
   311  		Dirnames: absContentDirs,
   312  	}
   313  
   314  	b.SourceFilesystems = sourceFilesystems
   315  	b.themeFs = builder.themeFs
   316  	b.AbsThemeDirs = builder.absThemeDirs
   317  
   318  	return b, nil
   319  }
   320  
   321  type sourceFilesystemsBuilder struct {
   322  	p            *paths.Paths
   323  	result       *SourceFilesystems
   324  	themeFs      afero.Fs
   325  	hasTheme     bool
   326  	absThemeDirs []string
   327  }
   328  
   329  func newSourceFilesystemsBuilder(p *paths.Paths, b *BaseFs) *sourceFilesystemsBuilder {
   330  	return &sourceFilesystemsBuilder{p: p, themeFs: b.themeFs, absThemeDirs: b.AbsThemeDirs, result: &SourceFilesystems{}}
   331  }
   332  
   333  func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
   334  	if b.themeFs == nil && b.p.ThemeSet() {
   335  		themeFs, absThemeDirs, err := createThemesOverlayFs(b.p)
   336  		if err != nil {
   337  			return nil, err
   338  		}
   339  		if themeFs == nil {
   340  			panic("createThemesFs returned nil")
   341  		}
   342  		b.themeFs = themeFs
   343  		b.absThemeDirs = absThemeDirs
   344  
   345  	}
   346  
   347  	b.hasTheme = len(b.absThemeDirs) > 0
   348  
   349  	sfs, err := b.createRootMappingFs("dataDir", "data")
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  	b.result.Data = sfs
   354  
   355  	sfs, err = b.createRootMappingFs("i18nDir", "i18n")
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  	b.result.I18n = sfs
   360  
   361  	sfs, err = b.createFs(false, true, "layoutDir", "layouts")
   362  	if err != nil {
   363  		return nil, err
   364  	}
   365  	b.result.Layouts = sfs
   366  
   367  	sfs, err = b.createFs(false, true, "archetypeDir", "archetypes")
   368  	if err != nil {
   369  		return nil, err
   370  	}
   371  	b.result.Archetypes = sfs
   372  
   373  	sfs, err = b.createFs(false, true, "assetDir", "assets")
   374  	if err != nil {
   375  		return nil, err
   376  	}
   377  	b.result.Assets = sfs
   378  
   379  	sfs, err = b.createFs(true, false, "resourceDir", "resources")
   380  	if err != nil {
   381  		return nil, err
   382  	}
   383  
   384  	b.result.Resources = sfs
   385  
   386  	err = b.createStaticFs()
   387  
   388  	sfs, err = b.createFs(false, true, "", "")
   389  	if err != nil {
   390  		return nil, err
   391  	}
   392  	b.result.Work = sfs
   393  
   394  	err = b.createStaticFs()
   395  	if err != nil {
   396  		return nil, err
   397  	}
   398  
   399  	return b.result, nil
   400  }
   401  
   402  func (b *sourceFilesystemsBuilder) createFs(
   403  	mkdir bool,
   404  	readOnly bool,
   405  	dirKey, themeFolder string) (*SourceFilesystem, error) {
   406  	s := &SourceFilesystem{
   407  		SourceFs: b.p.Fs.Source,
   408  	}
   409  
   410  	if themeFolder == "" {
   411  		themeFolder = filePathSeparator
   412  	}
   413  
   414  	var dir string
   415  	if dirKey != "" {
   416  		dir = b.p.Cfg.GetString(dirKey)
   417  		if dir == "" {
   418  			return s, fmt.Errorf("config %q not set", dirKey)
   419  		}
   420  	}
   421  
   422  	var fs afero.Fs
   423  
   424  	absDir := b.p.AbsPathify(dir)
   425  	existsInSource := b.existsInSource(absDir)
   426  	if !existsInSource && mkdir {
   427  		// We really need this directory. Make it.
   428  		if err := b.p.Fs.Source.MkdirAll(absDir, 0777); err == nil {
   429  			existsInSource = true
   430  		}
   431  	}
   432  	if existsInSource {
   433  		fs = newRealBase(afero.NewBasePathFs(b.p.Fs.Source, absDir))
   434  		s.Dirnames = []string{absDir}
   435  	}
   436  
   437  	if b.hasTheme {
   438  		themeFolderFs := newRealBase(afero.NewBasePathFs(b.themeFs, themeFolder))
   439  		if fs == nil {
   440  			fs = themeFolderFs
   441  		} else {
   442  			fs = afero.NewCopyOnWriteFs(themeFolderFs, fs)
   443  		}
   444  
   445  		for _, absThemeDir := range b.absThemeDirs {
   446  			absThemeFolderDir := filepath.Join(absThemeDir, themeFolder)
   447  			if b.existsInSource(absThemeFolderDir) {
   448  				s.Dirnames = append(s.Dirnames, absThemeFolderDir)
   449  			}
   450  		}
   451  	}
   452  
   453  	if fs == nil {
   454  		s.Fs = hugofs.NoOpFs
   455  	} else if readOnly {
   456  		s.Fs = afero.NewReadOnlyFs(fs)
   457  	} else {
   458  		s.Fs = fs
   459  	}
   460  
   461  	return s, nil
   462  }
   463  
   464  // Used for data, i18n -- we cannot use overlay filsesystems for those, but we need
   465  // to keep a strict order.
   466  func (b *sourceFilesystemsBuilder) createRootMappingFs(dirKey, themeFolder string) (*SourceFilesystem, error) {
   467  	s := &SourceFilesystem{
   468  		SourceFs: b.p.Fs.Source,
   469  	}
   470  
   471  	projectDir := b.p.Cfg.GetString(dirKey)
   472  	if projectDir == "" {
   473  		return nil, fmt.Errorf("config %q not set", dirKey)
   474  	}
   475  
   476  	var fromTo []string
   477  	to := b.p.AbsPathify(projectDir)
   478  
   479  	if b.existsInSource(to) {
   480  		s.Dirnames = []string{to}
   481  		fromTo = []string{projectVirtualFolder, to}
   482  	}
   483  
   484  	for _, theme := range b.p.AllThemes {
   485  		to := b.p.AbsPathify(filepath.Join(b.p.ThemesDir, theme.Name, themeFolder))
   486  		if b.existsInSource(to) {
   487  			s.Dirnames = append(s.Dirnames, to)
   488  			from := theme
   489  			fromTo = append(fromTo, from.Name, to)
   490  		}
   491  	}
   492  
   493  	if len(fromTo) == 0 {
   494  		s.Fs = hugofs.NoOpFs
   495  		return s, nil
   496  	}
   497  
   498  	fs, err := hugofs.NewRootMappingFs(b.p.Fs.Source, fromTo...)
   499  	if err != nil {
   500  		return nil, err
   501  	}
   502  
   503  	s.Fs = afero.NewReadOnlyFs(fs)
   504  
   505  	return s, nil
   506  }
   507  
   508  func (b *sourceFilesystemsBuilder) existsInSource(abspath string) bool {
   509  	exists, _ := afero.Exists(b.p.Fs.Source, abspath)
   510  	return exists
   511  }
   512  
   513  func (b *sourceFilesystemsBuilder) createStaticFs() error {
   514  	isMultihost := b.p.Cfg.GetBool("multihost")
   515  	ms := make(map[string]*SourceFilesystem)
   516  	b.result.Static = ms
   517  
   518  	if isMultihost {
   519  		for _, l := range b.p.Languages {
   520  			s := &SourceFilesystem{
   521  				SourceFs:      b.p.Fs.Source,
   522  				PublishFolder: l.Lang}
   523  			staticDirs := removeDuplicatesKeepRight(getStaticDirs(l))
   524  			if len(staticDirs) == 0 {
   525  				continue
   526  			}
   527  
   528  			for _, dir := range staticDirs {
   529  				absDir := b.p.AbsPathify(dir)
   530  				if !b.existsInSource(absDir) {
   531  					continue
   532  				}
   533  
   534  				s.Dirnames = append(s.Dirnames, absDir)
   535  			}
   536  
   537  			fs, err := createOverlayFs(b.p.Fs.Source, s.Dirnames)
   538  			if err != nil {
   539  				return err
   540  			}
   541  
   542  			if b.hasTheme {
   543  				themeFolder := "static"
   544  				fs = afero.NewCopyOnWriteFs(newRealBase(afero.NewBasePathFs(b.themeFs, themeFolder)), fs)
   545  				for _, absThemeDir := range b.absThemeDirs {
   546  					s.Dirnames = append(s.Dirnames, filepath.Join(absThemeDir, themeFolder))
   547  				}
   548  			}
   549  
   550  			s.Fs = fs
   551  			ms[l.Lang] = s
   552  
   553  		}
   554  
   555  		return nil
   556  	}
   557  
   558  	s := &SourceFilesystem{
   559  		SourceFs: b.p.Fs.Source,
   560  	}
   561  
   562  	var staticDirs []string
   563  
   564  	for _, l := range b.p.Languages {
   565  		staticDirs = append(staticDirs, getStaticDirs(l)...)
   566  	}
   567  
   568  	staticDirs = removeDuplicatesKeepRight(staticDirs)
   569  	if len(staticDirs) == 0 {
   570  		return nil
   571  	}
   572  
   573  	for _, dir := range staticDirs {
   574  		absDir := b.p.AbsPathify(dir)
   575  		if !b.existsInSource(absDir) {
   576  			continue
   577  		}
   578  		s.Dirnames = append(s.Dirnames, absDir)
   579  	}
   580  
   581  	fs, err := createOverlayFs(b.p.Fs.Source, s.Dirnames)
   582  	if err != nil {
   583  		return err
   584  	}
   585  
   586  	if b.hasTheme {
   587  		themeFolder := "static"
   588  		fs = afero.NewCopyOnWriteFs(newRealBase(afero.NewBasePathFs(b.themeFs, themeFolder)), fs)
   589  		for _, absThemeDir := range b.absThemeDirs {
   590  			s.Dirnames = append(s.Dirnames, filepath.Join(absThemeDir, themeFolder))
   591  		}
   592  	}
   593  
   594  	s.Fs = fs
   595  	ms[""] = s
   596  
   597  	return nil
   598  }
   599  
   600  func getStaticDirs(cfg config.Provider) []string {
   601  	var staticDirs []string
   602  	for i := -1; i <= 10; i++ {
   603  		staticDirs = append(staticDirs, getStringOrStringSlice(cfg, "staticDir", i)...)
   604  	}
   605  	return staticDirs
   606  }
   607  
   608  func getStringOrStringSlice(cfg config.Provider, key string, id int) []string {
   609  
   610  	if id >= 0 {
   611  		key = fmt.Sprintf("%s%d", key, id)
   612  	}
   613  
   614  	return config.GetStringSlicePreserveString(cfg, key)
   615  
   616  }
   617  
   618  func createContentFs(fs afero.Fs,
   619  	workingDir,
   620  	defaultContentLanguage string,
   621  	languages langs.Languages) (afero.Fs, []string, error) {
   622  
   623  	var contentLanguages langs.Languages
   624  	var contentDirSeen = make(map[string]bool)
   625  	languageSet := make(map[string]bool)
   626  
   627  	// The default content language needs to be first.
   628  	for _, language := range languages {
   629  		if language.Lang == defaultContentLanguage {
   630  			contentLanguages = append(contentLanguages, language)
   631  			contentDirSeen[language.ContentDir] = true
   632  		}
   633  		languageSet[language.Lang] = true
   634  	}
   635  
   636  	for _, language := range languages {
   637  		if contentDirSeen[language.ContentDir] {
   638  			continue
   639  		}
   640  		if language.ContentDir == "" {
   641  			language.ContentDir = defaultContentLanguage
   642  		}
   643  		contentDirSeen[language.ContentDir] = true
   644  		contentLanguages = append(contentLanguages, language)
   645  
   646  	}
   647  
   648  	var absContentDirs []string
   649  
   650  	fs, err := createContentOverlayFs(fs, workingDir, contentLanguages, languageSet, &absContentDirs)
   651  	return fs, absContentDirs, err
   652  
   653  }
   654  
   655  func createContentOverlayFs(source afero.Fs,
   656  	workingDir string,
   657  	languages langs.Languages,
   658  	languageSet map[string]bool,
   659  	absContentDirs *[]string) (afero.Fs, error) {
   660  	if len(languages) == 0 {
   661  		return source, nil
   662  	}
   663  
   664  	language := languages[0]
   665  
   666  	contentDir := language.ContentDir
   667  	if contentDir == "" {
   668  		panic("missing contentDir")
   669  	}
   670  
   671  	absContentDir := paths.AbsPathify(workingDir, language.ContentDir)
   672  	if !strings.HasSuffix(absContentDir, paths.FilePathSeparator) {
   673  		absContentDir += paths.FilePathSeparator
   674  	}
   675  
   676  	// If root, remove the second '/'
   677  	if absContentDir == "//" {
   678  		absContentDir = paths.FilePathSeparator
   679  	}
   680  
   681  	if len(absContentDir) < 6 {
   682  		return nil, fmt.Errorf("invalid content dir %q: Path is too short", absContentDir)
   683  	}
   684  
   685  	*absContentDirs = append(*absContentDirs, absContentDir)
   686  
   687  	overlay := hugofs.NewLanguageFs(language.Lang, languageSet, afero.NewBasePathFs(source, absContentDir))
   688  	if len(languages) == 1 {
   689  		return overlay, nil
   690  	}
   691  
   692  	base, err := createContentOverlayFs(source, workingDir, languages[1:], languageSet, absContentDirs)
   693  	if err != nil {
   694  		return nil, err
   695  	}
   696  
   697  	return hugofs.NewLanguageCompositeFs(base, overlay), nil
   698  
   699  }
   700  
   701  func createThemesOverlayFs(p *paths.Paths) (afero.Fs, []string, error) {
   702  
   703  	themes := p.AllThemes
   704  
   705  	if len(themes) == 0 {
   706  		panic("AllThemes not set")
   707  	}
   708  
   709  	themesDir := p.AbsPathify(p.ThemesDir)
   710  	if themesDir == "" {
   711  		return nil, nil, errors.New("no themes dir set")
   712  	}
   713  
   714  	absPaths := make([]string, len(themes))
   715  
   716  	// The themes are ordered from left to right. We need to revert it to get the
   717  	// overlay logic below working as expected.
   718  	for i := 0; i < len(themes); i++ {
   719  		absPaths[i] = filepath.Join(themesDir, themes[len(themes)-1-i].Name)
   720  	}
   721  
   722  	fs, err := createOverlayFs(p.Fs.Source, absPaths)
   723  
   724  	return fs, absPaths, err
   725  
   726  }
   727  
   728  func createOverlayFs(source afero.Fs, absPaths []string) (afero.Fs, error) {
   729  	if len(absPaths) == 0 {
   730  		return hugofs.NoOpFs, nil
   731  	}
   732  
   733  	if len(absPaths) == 1 {
   734  		return afero.NewReadOnlyFs(newRealBase(afero.NewBasePathFs(source, absPaths[0]))), nil
   735  	}
   736  
   737  	base := afero.NewReadOnlyFs(newRealBase(afero.NewBasePathFs(source, absPaths[0])))
   738  	overlay, err := createOverlayFs(source, absPaths[1:])
   739  	if err != nil {
   740  		return nil, err
   741  	}
   742  
   743  	return afero.NewCopyOnWriteFs(base, overlay), nil
   744  }
   745  
   746  func removeDuplicatesKeepRight(in []string) []string {
   747  	seen := make(map[string]bool)
   748  	var out []string
   749  	for i := len(in) - 1; i >= 0; i-- {
   750  		v := in[i]
   751  		if seen[v] {
   752  			continue
   753  		}
   754  		out = append([]string{v}, out...)
   755  		seen[v] = true
   756  	}
   757  
   758  	return out
   759  }
   760  
   761  func printFs(fs afero.Fs, path string, w io.Writer) {
   762  	if fs == nil {
   763  		return
   764  	}
   765  	afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error {
   766  		if info != nil && !info.IsDir() {
   767  			s := path
   768  			if lang, ok := info.(hugofs.LanguageAnnouncer); ok {
   769  				s = s + "\tLANG: " + lang.Lang()
   770  			}
   771  			if fp, ok := info.(hugofs.FilePather); ok {
   772  				s = s + "\tRF: " + fp.Filename() + "\tBP: " + fp.BaseDir()
   773  			}
   774  			fmt.Fprintln(w, "    ", s)
   775  		}
   776  		return nil
   777  	})
   778  }