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 }