configLoader.go (5387B)
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 config
15
16 import (
17 "fmt"
18 "os"
19 "path/filepath"
20 "strings"
21
22 "github.com/gohugoio/hugo/common/herrors"
23
24 "github.com/gohugoio/hugo/common/paths"
25
26 "github.com/gohugoio/hugo/common/maps"
27 "github.com/gohugoio/hugo/parser/metadecoders"
28 "github.com/spf13/afero"
29 )
30
31 var (
32 ValidConfigFileExtensions = []string{"toml", "yaml", "yml", "json"}
33 validConfigFileExtensionsMap map[string]bool = make(map[string]bool)
34 )
35
36 func init() {
37 for _, ext := range ValidConfigFileExtensions {
38 validConfigFileExtensionsMap[ext] = true
39 }
40 }
41
42 // IsValidConfigFilename returns whether filename is one of the supported
43 // config formats in Hugo.
44 func IsValidConfigFilename(filename string) bool {
45 ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
46 return validConfigFileExtensionsMap[ext]
47 }
48
49 // FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
50 func FromConfigString(config, configType string) (Provider, error) {
51 m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config))
52 if err != nil {
53 return nil, err
54 }
55 return NewFrom(m), nil
56 }
57
58 // FromFile loads the configuration from the given filename.
59 func FromFile(fs afero.Fs, filename string) (Provider, error) {
60 m, err := loadConfigFromFile(fs, filename)
61 if err != nil {
62 fe := herrors.UnwrapFileError(err)
63 if fe != nil {
64 pos := fe.Position()
65 pos.Filename = filename
66 fe.UpdatePosition(pos)
67 return nil, err
68 }
69 return nil, herrors.NewFileErrorFromFile(err, filename, fs, nil)
70 }
71 return NewFrom(m), nil
72 }
73
74 // FromFileToMap is the same as FromFile, but it returns the config values
75 // as a simple map.
76 func FromFileToMap(fs afero.Fs, filename string) (map[string]any, error) {
77 return loadConfigFromFile(fs, filename)
78 }
79
80 func readConfig(format metadecoders.Format, data []byte) (map[string]any, error) {
81 m, err := metadecoders.Default.UnmarshalToMap(data, format)
82 if err != nil {
83 return nil, err
84 }
85
86 RenameKeys(m)
87
88 return m, nil
89 }
90
91 func loadConfigFromFile(fs afero.Fs, filename string) (map[string]any, error) {
92 m, err := metadecoders.Default.UnmarshalFileToMap(fs, filename)
93 if err != nil {
94 return nil, err
95 }
96 RenameKeys(m)
97 return m, nil
98 }
99
100 func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provider, []string, error) {
101 defaultConfigDir := filepath.Join(configDir, "_default")
102 environmentConfigDir := filepath.Join(configDir, environment)
103 cfg := New()
104
105 var configDirs []string
106 // Merge from least to most specific.
107 for _, dir := range []string{defaultConfigDir, environmentConfigDir} {
108 if _, err := sourceFs.Stat(dir); err == nil {
109 configDirs = append(configDirs, dir)
110 }
111 }
112
113 if len(configDirs) == 0 {
114 return nil, nil, nil
115 }
116
117 // Keep track of these so we can watch them for changes.
118 var dirnames []string
119
120 for _, configDir := range configDirs {
121 err := afero.Walk(sourceFs, configDir, func(path string, fi os.FileInfo, err error) error {
122 if fi == nil || err != nil {
123 return nil
124 }
125
126 if fi.IsDir() {
127 dirnames = append(dirnames, path)
128 return nil
129 }
130
131 if !IsValidConfigFilename(path) {
132 return nil
133 }
134
135 name := paths.Filename(filepath.Base(path))
136
137 item, err := metadecoders.Default.UnmarshalFileToMap(sourceFs, path)
138 if err != nil {
139 // This will be used in error reporting, use the most specific value.
140 dirnames = []string{path}
141 return fmt.Errorf("failed to unmarshl config for path %q: %w", path, err)
142 }
143
144 var keyPath []string
145
146 if name != "config" {
147 // Can be params.jp, menus.en etc.
148 name, lang := paths.FileAndExtNoDelimiter(name)
149
150 keyPath = []string{name}
151
152 if lang != "" {
153 keyPath = []string{"languages", lang}
154 switch name {
155 case "menu", "menus":
156 keyPath = append(keyPath, "menus")
157 case "params":
158 keyPath = append(keyPath, "params")
159 }
160 }
161 }
162
163 root := item
164 if len(keyPath) > 0 {
165 root = make(map[string]any)
166 m := root
167 for i, key := range keyPath {
168 if i >= len(keyPath)-1 {
169 m[key] = item
170 } else {
171 nm := make(map[string]any)
172 m[key] = nm
173 m = nm
174 }
175 }
176 }
177
178 // Migrate menu => menus etc.
179 RenameKeys(root)
180
181 // Set will overwrite keys with the same name, recursively.
182 cfg.Set("", root)
183
184 return nil
185 })
186 if err != nil {
187 return nil, dirnames, err
188 }
189
190 }
191
192 return cfg, dirnames, nil
193
194 }
195
196 var keyAliases maps.KeyRenamer
197
198 func init() {
199 var err error
200 keyAliases, err = maps.NewKeyRenamer(
201 // Before 0.53 we used singular for "menu".
202 "{menu,languages/*/menu}", "menus",
203 )
204
205 if err != nil {
206 panic(err)
207 }
208 }
209
210 // RenameKeys renames config keys in m recursively according to a global Hugo
211 // alias definition.
212 func RenameKeys(m map[string]any) {
213 keyAliases.Rename(m)
214 }