filecache_config.go (6155B)
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 filecache 15 16 import ( 17 "fmt" 18 "path" 19 "path/filepath" 20 "strings" 21 "time" 22 23 "github.com/gohugoio/hugo/common/maps" 24 25 "github.com/gohugoio/hugo/config" 26 27 "github.com/gohugoio/hugo/helpers" 28 29 "errors" 30 31 "github.com/mitchellh/mapstructure" 32 "github.com/spf13/afero" 33 ) 34 35 const ( 36 cachesConfigKey = "caches" 37 38 resourcesGenDir = ":resourceDir/_gen" 39 cacheDirProject = ":cacheDir/:project" 40 ) 41 42 var defaultCacheConfig = Config{ 43 MaxAge: -1, // Never expire 44 Dir: cacheDirProject, 45 } 46 47 const ( 48 cacheKeyGetJSON = "getjson" 49 cacheKeyGetCSV = "getcsv" 50 cacheKeyImages = "images" 51 cacheKeyAssets = "assets" 52 cacheKeyModules = "modules" 53 cacheKeyGetResource = "getresource" 54 ) 55 56 type Configs map[string]Config 57 58 func (c Configs) CacheDirModules() string { 59 return c[cacheKeyModules].Dir 60 } 61 62 var defaultCacheConfigs = Configs{ 63 cacheKeyModules: { 64 MaxAge: -1, 65 Dir: ":cacheDir/modules", 66 }, 67 cacheKeyGetJSON: defaultCacheConfig, 68 cacheKeyGetCSV: defaultCacheConfig, 69 cacheKeyImages: { 70 MaxAge: -1, 71 Dir: resourcesGenDir, 72 }, 73 cacheKeyAssets: { 74 MaxAge: -1, 75 Dir: resourcesGenDir, 76 }, 77 cacheKeyGetResource: Config{ 78 MaxAge: -1, // Never expire 79 Dir: cacheDirProject, 80 }, 81 } 82 83 type Config struct { 84 // Max age of cache entries in this cache. Any items older than this will 85 // be removed and not returned from the cache. 86 // a negative value means forever, 0 means cache is disabled. 87 MaxAge time.Duration 88 89 // The directory where files are stored. 90 Dir string 91 92 // Will resources/_gen will get its own composite filesystem that 93 // also checks any theme. 94 isResourceDir bool 95 } 96 97 // GetJSONCache gets the file cache for getJSON. 98 func (f Caches) GetJSONCache() *Cache { 99 return f[cacheKeyGetJSON] 100 } 101 102 // GetCSVCache gets the file cache for getCSV. 103 func (f Caches) GetCSVCache() *Cache { 104 return f[cacheKeyGetCSV] 105 } 106 107 // ImageCache gets the file cache for processed images. 108 func (f Caches) ImageCache() *Cache { 109 return f[cacheKeyImages] 110 } 111 112 // ModulesCache gets the file cache for Hugo Modules. 113 func (f Caches) ModulesCache() *Cache { 114 return f[cacheKeyModules] 115 } 116 117 // AssetsCache gets the file cache for assets (processed resources, SCSS etc.). 118 func (f Caches) AssetsCache() *Cache { 119 return f[cacheKeyAssets] 120 } 121 122 // GetResourceCache gets the file cache for remote resources. 123 func (f Caches) GetResourceCache() *Cache { 124 return f[cacheKeyGetResource] 125 } 126 127 func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) { 128 c := make(Configs) 129 valid := make(map[string]bool) 130 // Add defaults 131 for k, v := range defaultCacheConfigs { 132 c[k] = v 133 valid[k] = true 134 } 135 136 m := cfg.GetStringMap(cachesConfigKey) 137 138 _, isOsFs := fs.(*afero.OsFs) 139 140 for k, v := range m { 141 if _, ok := v.(maps.Params); !ok { 142 continue 143 } 144 cc := defaultCacheConfig 145 146 dc := &mapstructure.DecoderConfig{ 147 Result: &cc, 148 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 149 WeaklyTypedInput: true, 150 } 151 152 decoder, err := mapstructure.NewDecoder(dc) 153 if err != nil { 154 return c, err 155 } 156 157 if err := decoder.Decode(v); err != nil { 158 return nil, fmt.Errorf("failed to decode filecache config: %w", err) 159 } 160 161 if cc.Dir == "" { 162 return c, errors.New("must provide cache Dir") 163 } 164 165 name := strings.ToLower(k) 166 if !valid[name] { 167 return nil, fmt.Errorf("%q is not a valid cache name", name) 168 } 169 170 c[name] = cc 171 } 172 173 // This is a very old flag in Hugo, but we need to respect it. 174 disabled := cfg.GetBool("ignoreCache") 175 176 for k, v := range c { 177 dir := filepath.ToSlash(filepath.Clean(v.Dir)) 178 hadSlash := strings.HasPrefix(dir, "/") 179 parts := strings.Split(dir, "/") 180 181 for i, part := range parts { 182 if strings.HasPrefix(part, ":") { 183 resolved, isResource, err := resolveDirPlaceholder(fs, cfg, part) 184 if err != nil { 185 return c, err 186 } 187 if isResource { 188 v.isResourceDir = true 189 } 190 parts[i] = resolved 191 } 192 } 193 194 dir = path.Join(parts...) 195 if hadSlash { 196 dir = "/" + dir 197 } 198 v.Dir = filepath.Clean(filepath.FromSlash(dir)) 199 200 if !v.isResourceDir { 201 if isOsFs && !filepath.IsAbs(v.Dir) { 202 return c, fmt.Errorf("%q must resolve to an absolute directory", v.Dir) 203 } 204 205 // Avoid cache in root, e.g. / (Unix) or c:\ (Windows) 206 if len(strings.TrimPrefix(v.Dir, filepath.VolumeName(v.Dir))) == 1 { 207 return c, fmt.Errorf("%q is a root folder and not allowed as cache dir", v.Dir) 208 } 209 } 210 211 if !strings.HasPrefix(v.Dir, "_gen") { 212 // We do cache eviction (file removes) and since the user can set 213 // his/hers own cache directory, we really want to make sure 214 // we do not delete any files that do not belong to this cache. 215 // We do add the cache name as the root, but this is an extra safe 216 // guard. We skip the files inside /resources/_gen/ because 217 // that would be breaking. 218 v.Dir = filepath.Join(v.Dir, filecacheRootDirname, k) 219 } else { 220 v.Dir = filepath.Join(v.Dir, k) 221 } 222 223 if disabled { 224 v.MaxAge = 0 225 } 226 227 c[k] = v 228 } 229 230 return c, nil 231 } 232 233 // Resolves :resourceDir => /myproject/resources etc., :cacheDir => ... 234 func resolveDirPlaceholder(fs afero.Fs, cfg config.Provider, placeholder string) (cacheDir string, isResource bool, err error) { 235 workingDir := cfg.GetString("workingDir") 236 237 switch strings.ToLower(placeholder) { 238 case ":resourcedir": 239 return "", true, nil 240 case ":cachedir": 241 d, err := helpers.GetCacheDir(fs, cfg) 242 return d, false, err 243 case ":project": 244 return filepath.Base(workingDir), false, nil 245 } 246 247 return "", false, fmt.Errorf("%q is not a valid placeholder (valid values are :cacheDir or :resourceDir)", placeholder) 248 }