diff options
Diffstat (limited to 'hugolib')
-rw-r--r-- | hugolib/alias_test.go | 4 | ||||
-rw-r--r-- | hugolib/case_insensitive_test.go | 3 | ||||
-rw-r--r-- | hugolib/config.go | 145 | ||||
-rw-r--r-- | hugolib/datafiles_test.go | 4 | ||||
-rw-r--r-- | hugolib/filesystems/basefs.go | 644 | ||||
-rw-r--r-- | hugolib/filesystems/basefs_test.go | 170 | ||||
-rw-r--r-- | hugolib/hugo_sites.go | 8 | ||||
-rw-r--r-- | hugolib/hugo_sites_build_test.go | 28 | ||||
-rw-r--r-- | hugolib/hugo_sites_multihost_test.go | 3 | ||||
-rw-r--r-- | hugolib/hugo_themes_test.go | 268 | ||||
-rw-r--r-- | hugolib/multilingual.go | 43 | ||||
-rw-r--r-- | hugolib/page.go | 10 | ||||
-rw-r--r-- | hugolib/page_bundler_capture.go | 2 | ||||
-rw-r--r-- | hugolib/page_bundler_capture_test.go | 12 | ||||
-rw-r--r-- | hugolib/page_bundler_test.go | 8 | ||||
-rw-r--r-- | hugolib/pagination.go | 2 | ||||
-rw-r--r-- | hugolib/paths/baseURL.go | 79 | ||||
-rw-r--r-- | hugolib/paths/baseURL_test.go | 61 | ||||
-rw-r--r-- | hugolib/paths/paths.go | 231 | ||||
-rw-r--r-- | hugolib/paths/paths_test.go | 40 | ||||
-rw-r--r-- | hugolib/paths/themes.go | 162 | ||||
-rw-r--r-- | hugolib/shortcode_test.go | 4 | ||||
-rw-r--r-- | hugolib/site.go | 188 | ||||
-rw-r--r-- | hugolib/testhelpers_test.go | 47 |
24 files changed, 1849 insertions, 317 deletions
diff --git a/hugolib/alias_test.go b/hugolib/alias_test.go index d20409512..04c5b4358 100644 --- a/hugolib/alias_test.go +++ b/hugolib/alias_test.go @@ -18,6 +18,8 @@ import ( "runtime" "testing" + "github.com/gohugoio/hugo/common/loggers" + "github.com/stretchr/testify/require" ) @@ -97,7 +99,7 @@ func TestAliasTemplate(t *testing.T) { } func TestTargetPathHTMLRedirectAlias(t *testing.T) { - h := newAliasHandler(nil, newErrorLogger(), false) + h := newAliasHandler(nil, loggers.NewErrorLogger(), false) errIsNilForThisOS := runtime.GOOS != "windows" diff --git a/hugolib/case_insensitive_test.go b/hugolib/case_insensitive_test.go index 52ef198a5..f3ba5f933 100644 --- a/hugolib/case_insensitive_test.go +++ b/hugolib/case_insensitive_test.go @@ -19,8 +19,9 @@ import ( "strings" "testing" - "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugofs" + + "github.com/gohugoio/hugo/deps" "github.com/spf13/afero" "github.com/stretchr/testify/require" ) diff --git a/hugolib/config.go b/hugolib/config.go index 73ba84686..dec5b870d 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -16,11 +16,14 @@ package hugolib import ( "errors" "fmt" - "path/filepath" + + "github.com/gohugoio/hugo/hugolib/paths" "io" "strings" + "github.com/gohugoio/hugo/langs" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/privacy" "github.com/gohugoio/hugo/config/services" @@ -81,6 +84,8 @@ func LoadConfigDefault(fs afero.Fs) (*viper.Viper, error) { return v, err } +var ErrNoConfigFile = errors.New("Unable to locate Config file. Perhaps you need to create a new site.\n Run `hugo help new` for details.\n") + // LoadConfig loads Hugo configuration into a new Viper and then adds // a set of defaults. func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (*viper.Viper, []string, error) { @@ -100,41 +105,50 @@ func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provid v.SetConfigFile(configFilenames[0]) v.AddConfigPath(d.Path) + var configFileErr error + err := v.ReadInConfig() if err != nil { if _, ok := err.(viper.ConfigParseError); ok { return nil, configFiles, err } - return nil, configFiles, fmt.Errorf("Unable to locate Config file. Perhaps you need to create a new site.\n Run `hugo help new` for details. (%s)\n", err) + configFileErr = ErrNoConfigFile } - if cf := v.ConfigFileUsed(); cf != "" { - configFiles = append(configFiles, cf) - } + if configFileErr == nil { - for _, configFile := range configFilenames[1:] { - var r io.Reader - var err error - if r, err = fs.Open(configFile); err != nil { - return nil, configFiles, fmt.Errorf("Unable to open Config file.\n (%s)\n", err) + if cf := v.ConfigFileUsed(); cf != "" { + configFiles = append(configFiles, cf) } - if err = v.MergeConfig(r); err != nil { - return nil, configFiles, fmt.Errorf("Unable to parse/merge Config file (%s).\n (%s)\n", configFile, err) + + for _, configFile := range configFilenames[1:] { + var r io.Reader + var err error + if r, err = fs.Open(configFile); err != nil { + return nil, configFiles, fmt.Errorf("Unable to open Config file.\n (%s)\n", err) + } + if err = v.MergeConfig(r); err != nil { + return nil, configFiles, fmt.Errorf("Unable to parse/merge Config file (%s).\n (%s)\n", configFile, err) + } + configFiles = append(configFiles, configFile) } - configFiles = append(configFiles, configFile) + } if err := loadDefaultSettingsFor(v); err != nil { return v, configFiles, err } - themeConfigFile, err := loadThemeConfig(d, v) - if err != nil { - return v, configFiles, err - } + if configFileErr == nil { - if themeConfigFile != "" { - configFiles = append(configFiles, themeConfigFile) + themeConfigFiles, err := loadThemeConfig(d, v) + if err != nil { + return v, configFiles, err + } + + if len(themeConfigFiles) > 0 { + configFiles = append(configFiles, themeConfigFiles...) + } } // We create languages based on the settings, so we need to make sure that @@ -149,11 +163,11 @@ func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provid return v, configFiles, err } - return v, configFiles, nil + return v, configFiles, configFileErr } -func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error { +func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error { defaultLang := cfg.GetString("defaultContentLanguage") @@ -182,14 +196,14 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error } var ( - langs helpers.Languages - err error + languages2 langs.Languages + err error ) if len(languages) == 0 { - langs = append(langs, helpers.NewDefaultLanguage(cfg)) + languages2 = append(languages2, langs.NewDefaultLanguage(cfg)) } else { - langs, err = toSortedLanguages(cfg, languages) + languages2, err = toSortedLanguages(cfg, languages) if err != nil { return fmt.Errorf("Failed to parse multilingual config: %s", err) } @@ -201,10 +215,10 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error // The validation below isn't complete, but should cover the most // important cases. var invalid bool - if langs.IsMultihost() != oldLangs.IsMultihost() { + if languages2.IsMultihost() != oldLangs.IsMultihost() { invalid = true } else { - if langs.IsMultihost() && len(langs) != len(oldLangs) { + if languages2.IsMultihost() && len(languages2) != len(oldLangs) { invalid = true } } @@ -213,10 +227,10 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error return errors.New("language change needing a server restart detected") } - if langs.IsMultihost() { + if languages2.IsMultihost() { // We need to transfer any server baseURL to the new language for i, ol := range oldLangs { - nl := langs[i] + nl := languages2[i] nl.Set("baseURL", ol.GetString("baseURL")) } } @@ -225,7 +239,7 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error // The defaultContentLanguage is something the user has to decide, but it needs // to match a language in the language definition list. langExists := false - for _, lang := range langs { + for _, lang := range languages2 { if lang.Lang == defaultLang { langExists = true break @@ -236,10 +250,10 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error return fmt.Errorf("site config value %q for defaultContentLanguage does not match any language definition", defaultLang) } - cfg.Set("languagesSorted", langs) - cfg.Set("multilingual", len(langs) > 1) + cfg.Set("languagesSorted", languages2) + cfg.Set("multilingual", len(languages2) > 1) - multihost := langs.IsMultihost() + multihost := languages2.IsMultihost() if multihost { cfg.Set("defaultContentLanguageInSubdir", true) @@ -250,7 +264,7 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error // The baseURL may be provided at the language level. If that is true, // then every language must have a baseURL. In this case we always render // to a language sub folder, which is then stripped from all the Permalink URLs etc. - for _, l := range langs { + for _, l := range languages2 { burl := l.GetLocal("baseURL") if burl == nil { return errors.New("baseURL must be set on all or none of the languages") @@ -262,49 +276,32 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error return nil } -func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) (string, error) { +func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) ([]string, error) { + themesDir := paths.AbsPathify(d.WorkingDir, v1.GetString("themesDir")) + themes := config.GetStringSlicePreserveString(v1, "theme") - theme := v1.GetString("theme") - if theme == "" { - return "", nil + // CollectThemes(fs afero.Fs, themesDir string, themes []strin + themeConfigs, err := paths.CollectThemes(d.Fs, themesDir, themes) + if err != nil { + return nil, err } - - themesDir := helpers.AbsPathify(d.WorkingDir, v1.GetString("themesDir")) - configDir := filepath.Join(themesDir, theme) - - var ( - configPath string - exists bool - err error - ) - - // Viper supports more, but this is the sub-set supported by Hugo. - for _, configFormats := range []string{"toml", "yaml", "yml", "json"} { - configPath = filepath.Join(configDir, "config."+configFormats) - exists, err = helpers.Exists(configPath, d.Fs) - if err != nil { - return "", err - } - if exists { - break + v1.Set("allThemes", themeConfigs) + + var configFilenames []string + for _, tc := range themeConfigs { + if tc.ConfigFilename != "" { + configFilenames = append(configFilenames, tc.ConfigFilename) + if err := applyThemeConfig(v1, tc); err != nil { + return nil, err + } } } - if !exists { - // No theme config set. - return "", nil - } + return configFilenames, nil - v2 := viper.New() - v2.SetFs(d.Fs) - v2.AutomaticEnv() - v2.SetEnvPrefix("hugo") - v2.SetConfigFile(configPath) +} - err = v2.ReadInConfig() - if err != nil { - return "", err - } +func applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) error { const ( paramsKey = "params" @@ -312,11 +309,13 @@ func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) (string, error) menuKey = "menu" ) + v2 := theme.Cfg + for _, key := range []string{paramsKey, "outputformats", "mediatypes"} { mergeStringMapKeepLeft("", key, v1, v2) } - themeLower := strings.ToLower(theme) + themeLower := strings.ToLower(theme.Name) themeParamsNamespace := paramsKey + "." + themeLower // Set namespaced params @@ -371,11 +370,11 @@ func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) (string, error) } } - return v2.ConfigFileUsed(), nil + return nil } -func mergeStringMapKeepLeft(rootKey, key string, v1, v2 *viper.Viper) { +func mergeStringMapKeepLeft(rootKey, key string, v1, v2 config.Provider) { if !v2.IsSet(key) { return } diff --git a/hugolib/datafiles_test.go b/hugolib/datafiles_test.go index cd1ad8411..8b2dc8c0f 100644 --- a/hugolib/datafiles_test.go +++ b/hugolib/datafiles_test.go @@ -19,6 +19,8 @@ import ( "strings" "testing" + "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/deps" "fmt" @@ -322,7 +324,7 @@ func doTestDataDirImpl(t *testing.T, dd dataDir, expected interface{}, configKey } var ( - logger = newErrorLogger() + logger = loggers.NewErrorLogger() depsCfg = deps.DepsCfg{Fs: fs, Cfg: cfg, Logger: logger} ) diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go new file mode 100644 index 000000000..deecd69a5 --- /dev/null +++ b/hugolib/filesystems/basefs.go @@ -0,0 +1,644 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package filesystems provides the fine grained file systems used by Hugo. These +// are typically virtual filesystems that are composites of project and theme content. +package filesystems + +import ( + "errors" + "io" + "os" + "path/filepath" + "strings" + + "github.com/gohugoio/hugo/config" + + "github.com/gohugoio/hugo/hugofs" + + "fmt" + + "github.com/gohugoio/hugo/common/types" + "github.com/gohugoio/hugo/hugolib/paths" + "github.com/gohugoio/hugo/langs" + "github.com/spf13/afero" +) + +// When we create a virtual filesystem with data and i18n bundles for the project and the themes, +// this is the name of the project's virtual root. It got it's funky name to make sure +// (or very unlikely) that it collides with a theme name. +const projectVirtualFolder = "__h__project" + +var filePathSeparator = string(filepath.Separator) + +// BaseFs contains the core base filesystems used by Hugo. The name "base" is used +// to underline that even if they can be composites, they all have a base path set to a specific +// resource folder, e.g "/my-project/content". So, no absolute filenames needed. +type BaseFs struct { + // TODO(bep) make this go away + AbsContentDirs []types.KeyValueStr + + // The filesystem used to capture content. This can be a composite and + // language aware file system. + ContentFs afero.Fs + + // SourceFilesystems contains the different source file systems. + *SourceFilesystems + + // The filesystem used to store resources (processed images etc.). + // This usually maps to /my-project/resources. + ResourcesFs afero.Fs + + // The filesystem used to publish the rendered site. + // This usually maps to /my-project/public. + PublishFs afero.Fs + + themeFs afero.Fs + + // TODO(bep) improve the "theme interaction" + AbsThemeDirs []string +} + +// RelContentDir tries to create a path relative to the content root from +// the given filename. The return value is the path and language code. +func (b *BaseFs) RelContentDir(filename string) (string, string) { + for _, dir := range b.AbsContentDirs { + if strings.HasPrefix(filename, dir.Value) { + rel := strings.TrimPrefix(filename, dir.Value) + return strings.TrimPrefix(rel, filePathSeparator), dir.Key + } + } + // Either not a content dir or already relative. + return filename, "" +} + +// IsContent returns whether the given filename is in the content filesystem. +func (b *BaseFs) IsContent(filename string) bool { + for _, dir := range b.AbsContentDirs { + if strings.HasPrefix(filename, dir.Value) { + return true + } + } + return false +} + +// SourceFilesystems contains the different source file systems. These can be +// composite file systems (theme and project etc.), and they have all root +// set to the source type the provides: data, i18n, static, layouts. +type SourceFilesystems struct { + Data *SourceFilesystem + I18n *SourceFilesystem + Layouts *SourceFilesystem + Archetypes *SourceFilesystem + + // When in multihost we have one static filesystem per language. The sync + // static files is currently done outside of the Hugo build (where there is + // a concept of a site per language). + // When in non-multihost mode there will be one entry in this map with a blank key. + Static map[string]*SourceFilesystem +} + +// A SourceFilesystem holds the filesystem for a given source type in Hugo (data, +// i18n, layouts, static) and additional metadata to be able to use that filesystem +// in server mode. +type SourceFilesystem struct { + Fs afero.Fs + + Dirnames []string + + // When syncing a source folder to the target (e.g. /public), this may + // be set to publish into a subfolder. This is used for static syncing + // in multihost mode. + PublishFolder string +} + +// IsStatic returns true if the given filename is a member of one of the static +// filesystems. +func (s SourceFilesystems) IsStatic(filename string) bool { + for _, staticFs := range s.Static { + if staticFs.Contains(filename) { + return true + } + } + return false +} + +// IsLayout returns true if the given filename is a member of the layouts filesystem. +func (s SourceFilesystems) IsLayout(filename string) bool { + return s.Layouts.Contains(filename) +} + +// IsData returns true if the given filename is a member of the data filesystem. +func (s SourceFilesystems) IsData(filename string) bool { + return s.Data.Contains(filename) +} + +// IsI18n returns true if the given filename is a member of the i18n filesystem. +func (s SourceFilesystems) IsI18n(filename string) bool { + return s.I18n.Contains(filename) +} + +// MakeStaticPathRelative makes an absolute static filename into a relative one. +// It will return an empty string if the filename is not a member of a static filesystem. +func (s SourceFilesystems) MakeStaticPathRelative(filename string) string { + for _, staticFs := range s.Static { + rel := staticFs.MakePathRelative(filename) + if rel != "" { + return rel + } + } + return "" +} + +// MakePathRelative creates a relative path from the given filename. +// It will return an empty string if the filename is not a member of this filesystem. +func (d *SourceFilesystem) MakePathRelative(filename string) string { + for _, currentPath := range d.Dirnames { + if strings.HasPrefix(filename, currentPath) { + return strings.TrimPrefix(filename, currentPath) + } + } + return "" +} + +// Contains returns whether the given filename is a member of the current filesystem. +func (d *SourceFilesystem) Contains(filename string) bool { + for _, dir := range d.Dirnames { + if strings.HasPrefix(filename, dir) { + return true + } + } + return false +} + +// WithBaseFs allows reuse of some potentially expensive to create parts that remain +// the same across sites/languages. +func WithBaseFs(b *BaseFs) func(*BaseFs) error { + return func(bb *BaseFs) error { + bb.themeFs = b.themeFs + bb.AbsThemeDirs = b.AbsThemeDirs + return nil + } +} + +// NewBase builds the filesystems used by Hugo given the paths and options provided.NewBase +func NewBase(p *paths.Paths, options ...func(*BaseFs) error) (*BaseFs, error) { + fs := p.Fs + + resourcesFs := afero.NewBasePathFs(fs.Source, p.AbsResourcesDir) + publishFs := afero.NewBasePathFs(fs.Destination, p.AbsPublishDir) + + contentFs, absContentDirs, err := createContentFs(fs.Source, p.WorkingDir, p.DefaultContentLanguage, p.Languages) + if err != nil { + return nil, err + } + + // Make sure we don't have any overlapping content dirs. That will never work. + for i, d1 := range absContentDirs { + for j, d2 := range absContentDirs { + if i == j { + continue + } + if strings.HasPrefix(d1.Value, d2.Value) || strings.HasPrefix(d2.Value, d1.Value) { + return nil, fmt.Errorf("found overlapping content dirs (%q and %q)", d1, d2) + } + } + } + + b := &BaseFs{ + AbsContentDirs: absContentDirs, + ContentFs: contentFs, + ResourcesFs: resourcesFs, + PublishFs: publishFs, + } + + for _, opt := range options { + if err := opt(b); err != nil { + return nil, err + } + } + + builder := newSourceFilesystemsBuilder(p, b) + sourceFilesystems, err := builder.Build() + if err != nil { + return nil, err + } + + b.SourceFilesystems = sourceFilesystems + b.themeFs = builder.themeFs + b.AbsThemeDirs = builder.absThemeDirs + + return b, nil +} + +type sourceFilesystemsBuilder struct { + p *paths.Paths + result *SourceFilesystems + themeFs afero.Fs + hasTheme bool + absThemeDirs []string +} + +func newSourceFilesystemsBuilder(p *paths.Paths, b *BaseFs) *sourceFilesystemsBuilder { + return &sourceFilesystemsBuilder{p: p, themeFs: b.themeFs, absThemeDirs: b.AbsThemeDirs, result: &SourceFilesystems{}} +} + +func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) { + if b.themeFs == nil && b.p.ThemeSet() { + themeFs, absThemeDirs, err := createThemesOverlayFs(b.p) + if err != nil { + return nil, err + } + if themeFs == nil { + panic("createThemesFs returned nil") + } + b.themeFs = themeFs + b.absThemeDirs = absThemeDirs + + } + + b.hasTheme = len(b.absThemeDirs) > 0 + + sfs, err := b.createRootMappingFs("dataDir", "data") + if err != nil { + return nil, err + } + b.result.Data = sfs + + sfs, err = b.createRootMappingFs("i18nDir", "i18n") + if err != nil { + return nil, err + } + b.result.I18n = sfs + + sfs, err = b.createFs("layoutDir", "layouts") + if err != nil { + return nil, err + } + b.result.Layouts = sfs + + sfs, err = b.createFs("archetypeDir", "archetypes") + if err != nil { + return nil, err + } + b.result.Archetypes = sfs + + err = b.createStaticFs() + if err != nil { + return nil, err + } + + return b.result, nil +} + +func (b *sourceFilesystemsBuilder) createFs(dirKey, themeFolder string) (*SourceFilesystem, error) { + s := &SourceFilesystem{} + dir := b.p.Cfg.GetString(dirKey) + if dir == "" { + return s, fmt.Errorf("config %q not set", dirKey) + } + + var fs afero.Fs + + absDir := b.p.AbsPathify(dir) + if b.existsInSource(absDir) { + fs = afero.NewBasePathFs(b.p.Fs.Source, absDir) + s.Dirnames = []string{absDir} + } + + if b.hasTheme { + themeFolderFs := afero.NewBasePathFs(b.themeFs, themeFolder) + if fs == nil { + fs = themeFolderFs + } else { + fs = afero.NewCopyOnWriteFs(themeFolderFs, fs) + } + + for _, absThemeDir := range b.absThemeDirs { + absThemeFolderDir := filepath.Join(absThemeDir, themeFolder) + if b.existsInSource(absThemeFolderDir) { + s.Dirnames = append(s.Dirnames, absThemeFolderDir) + } + } + } + + if fs == nil { + s.Fs = hugofs.NoOpFs + } else { + s.Fs = afero.NewReadOnlyFs(fs) + } + + return s, nil +} + +// Used for data, i18n -- we cannot use overlay filsesystems for those, but we need +// to keep a strict order. +func (b *sourceFilesystemsBuilder) createRootMappingFs(dirKey, themeFolder string) (*SourceFilesystem, error) { + s := &SourceFilesystem{} + + projectDir := b.p.Cfg.GetString(dirKey) + if projectDir == "" { + return nil, fmt.Errorf("config %q not set", dirKey) + } + + var fromTo []string + to := b.p.AbsPathify(projectDir) + + if b.existsInSource(to) { + s.Dirnames = []string{to} + fromTo = []string{projectVirtualFolder, to} + } + + for _, theme := range b.p.AllThemes { + to := b.p.AbsPathify(filepath.Join(b.p.ThemesDir, theme.Name, themeFolder)) + if b.existsInSource(to) { + s.Dirnames = append(s.Dirnames, to) + from := theme + fromTo = append(fromTo, from.Name, to) + } + } + + if len(fromTo) == 0 { + s.Fs = hugofs.NoOpFs + return s, nil + } + + fs, err := hugofs.NewRootMappingFs(b.p.Fs.Source, fromTo...) + if err != nil { + return nil, err + } + + s.Fs = afero.NewReadOnlyFs(fs) + + return s, nil + +} + +func (b *sourceFilesystemsBuilder) existsInSource(abspath string) bool { + exists, _ := afero.Exists(b.p.Fs.Source, abspath) + return exists +} + +func (b *sourceFilesystemsBuilder) createStaticFs() error { + isMultihost := b.p.Cfg.GetBool("multihost") + ms := make(map[string]*SourceFilesystem) + b.result.Static = ms + + if isMultihost { + for _, l := range b.p.Languages { + s := &SourceFilesystem{PublishFolder: l.Lang} + staticDirs := removeDuplicatesKeepRight(getStaticDirs(l)) + if len(staticDirs) == 0 { + continue + } + + for _, dir := range staticDirs { + absDir := b.p.AbsPathify(dir) + if !b.existsInSource(absDir) { + continue + } + + s.Dirnames = append(s.Dirnames, absDir) + } + + fs, err := createOverlayFs(b.p.Fs.Source, s.Dirnames) + if err != nil { + return err + } + + s.Fs = fs + ms[l.Lang] = s + |