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 }