hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

resource_cache.go (7149B)

    1 // Copyright 2019 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 resources
   15 
   16 import (
   17 	"encoding/json"
   18 	"io"
   19 	"path"
   20 	"path/filepath"
   21 	"regexp"
   22 	"strings"
   23 	"sync"
   24 
   25 	"github.com/gohugoio/hugo/helpers"
   26 
   27 	"github.com/gohugoio/hugo/hugofs/glob"
   28 
   29 	"github.com/gohugoio/hugo/resources/resource"
   30 
   31 	"github.com/gohugoio/hugo/cache/filecache"
   32 
   33 	"github.com/BurntSushi/locker"
   34 )
   35 
   36 const (
   37 	CACHE_CLEAR_ALL = "clear_all"
   38 	CACHE_OTHER     = "other"
   39 )
   40 
   41 type ResourceCache struct {
   42 	rs *Spec
   43 
   44 	sync.RWMutex
   45 
   46 	// Either resource.Resource or resource.Resources.
   47 	cache map[string]any
   48 
   49 	fileCache *filecache.Cache
   50 
   51 	// Provides named resource locks.
   52 	nlocker *locker.Locker
   53 }
   54 
   55 // ResourceCacheKey converts the filename into the format used in the resource
   56 // cache.
   57 func ResourceCacheKey(filename string) string {
   58 	filename = filepath.ToSlash(filename)
   59 	return path.Join(resourceKeyPartition(filename), filename)
   60 }
   61 
   62 func resourceKeyPartition(filename string) string {
   63 	ext := strings.TrimPrefix(path.Ext(filepath.ToSlash(filename)), ".")
   64 	if ext == "" {
   65 		ext = CACHE_OTHER
   66 	}
   67 	return ext
   68 }
   69 
   70 // Commonly used aliases and directory names used for some types.
   71 var extAliasKeywords = map[string][]string{
   72 	"sass": {"scss"},
   73 	"scss": {"sass"},
   74 }
   75 
   76 // ResourceKeyPartitions resolves a ordered slice of partitions that is
   77 // used to do resource cache invalidations.
   78 //
   79 // We use the first directory path element and the extension, so:
   80 //     a/b.json => "a", "json"
   81 //     b.json => "json"
   82 //
   83 // For some of the extensions we will also map to closely related types,
   84 // e.g. "scss" will also return "sass".
   85 //
   86 func ResourceKeyPartitions(filename string) []string {
   87 	var partitions []string
   88 	filename = glob.NormalizePath(filename)
   89 	dir, name := path.Split(filename)
   90 	ext := strings.TrimPrefix(path.Ext(filepath.ToSlash(name)), ".")
   91 
   92 	if dir != "" {
   93 		partitions = append(partitions, strings.Split(dir, "/")[0])
   94 	}
   95 
   96 	if ext != "" {
   97 		partitions = append(partitions, ext)
   98 	}
   99 
  100 	if aliases, found := extAliasKeywords[ext]; found {
  101 		partitions = append(partitions, aliases...)
  102 	}
  103 
  104 	if len(partitions) == 0 {
  105 		partitions = []string{CACHE_OTHER}
  106 	}
  107 
  108 	return helpers.UniqueStringsSorted(partitions)
  109 }
  110 
  111 // ResourceKeyContainsAny returns whether the key is a member of any of the
  112 // given partitions.
  113 //
  114 // This is used for resource cache invalidation.
  115 func ResourceKeyContainsAny(key string, partitions []string) bool {
  116 	parts := strings.Split(key, "/")
  117 	for _, p1 := range partitions {
  118 		for _, p2 := range parts {
  119 			if p1 == p2 {
  120 				return true
  121 			}
  122 		}
  123 	}
  124 	return false
  125 }
  126 
  127 func newResourceCache(rs *Spec) *ResourceCache {
  128 	return &ResourceCache{
  129 		rs:        rs,
  130 		fileCache: rs.FileCaches.AssetsCache(),
  131 		cache:     make(map[string]any),
  132 		nlocker:   locker.NewLocker(),
  133 	}
  134 }
  135 
  136 func (c *ResourceCache) clear() {
  137 	c.Lock()
  138 	defer c.Unlock()
  139 
  140 	c.cache = make(map[string]any)
  141 	c.nlocker = locker.NewLocker()
  142 }
  143 
  144 func (c *ResourceCache) Contains(key string) bool {
  145 	key = c.cleanKey(filepath.ToSlash(key))
  146 	_, found := c.get(key)
  147 	return found
  148 }
  149 
  150 func (c *ResourceCache) cleanKey(key string) string {
  151 	return strings.TrimPrefix(path.Clean(strings.ToLower(key)), "/")
  152 }
  153 
  154 func (c *ResourceCache) get(key string) (any, bool) {
  155 	c.RLock()
  156 	defer c.RUnlock()
  157 	r, found := c.cache[key]
  158 	return r, found
  159 }
  160 
  161 func (c *ResourceCache) GetOrCreate(key string, f func() (resource.Resource, error)) (resource.Resource, error) {
  162 	r, err := c.getOrCreate(key, func() (any, error) { return f() })
  163 	if r == nil || err != nil {
  164 		return nil, err
  165 	}
  166 	return r.(resource.Resource), nil
  167 }
  168 
  169 func (c *ResourceCache) GetOrCreateResources(key string, f func() (resource.Resources, error)) (resource.Resources, error) {
  170 	r, err := c.getOrCreate(key, func() (any, error) { return f() })
  171 	if r == nil || err != nil {
  172 		return nil, err
  173 	}
  174 	return r.(resource.Resources), nil
  175 }
  176 
  177 func (c *ResourceCache) getOrCreate(key string, f func() (any, error)) (any, error) {
  178 	key = c.cleanKey(key)
  179 	// First check in-memory cache.
  180 	r, found := c.get(key)
  181 	if found {
  182 		return r, nil
  183 	}
  184 	// This is a potentially long running operation, so get a named lock.
  185 	c.nlocker.Lock(key)
  186 
  187 	// Double check in-memory cache.
  188 	r, found = c.get(key)
  189 	if found {
  190 		c.nlocker.Unlock(key)
  191 		return r, nil
  192 	}
  193 
  194 	defer c.nlocker.Unlock(key)
  195 
  196 	r, err := f()
  197 	if err != nil {
  198 		return nil, err
  199 	}
  200 
  201 	c.set(key, r)
  202 
  203 	return r, nil
  204 }
  205 
  206 func (c *ResourceCache) getFilenames(key string) (string, string) {
  207 	filenameMeta := key + ".json"
  208 	filenameContent := key + ".content"
  209 
  210 	return filenameMeta, filenameContent
  211 }
  212 
  213 func (c *ResourceCache) getFromFile(key string) (filecache.ItemInfo, io.ReadCloser, transformedResourceMetadata, bool) {
  214 	c.RLock()
  215 	defer c.RUnlock()
  216 
  217 	var meta transformedResourceMetadata
  218 	filenameMeta, filenameContent := c.getFilenames(key)
  219 
  220 	_, jsonContent, _ := c.fileCache.GetBytes(filenameMeta)
  221 	if jsonContent == nil {
  222 		return filecache.ItemInfo{}, nil, meta, false
  223 	}
  224 
  225 	if err := json.Unmarshal(jsonContent, &meta); err != nil {
  226 		return filecache.ItemInfo{}, nil, meta, false
  227 	}
  228 
  229 	fi, rc, _ := c.fileCache.Get(filenameContent)
  230 
  231 	return fi, rc, meta, rc != nil
  232 }
  233 
  234 // writeMeta writes the metadata to file and returns a writer for the content part.
  235 func (c *ResourceCache) writeMeta(key string, meta transformedResourceMetadata) (filecache.ItemInfo, io.WriteCloser, error) {
  236 	filenameMeta, filenameContent := c.getFilenames(key)
  237 	raw, err := json.Marshal(meta)
  238 	if err != nil {
  239 		return filecache.ItemInfo{}, nil, err
  240 	}
  241 
  242 	_, fm, err := c.fileCache.WriteCloser(filenameMeta)
  243 	if err != nil {
  244 		return filecache.ItemInfo{}, nil, err
  245 	}
  246 	defer fm.Close()
  247 
  248 	if _, err := fm.Write(raw); err != nil {
  249 		return filecache.ItemInfo{}, nil, err
  250 	}
  251 
  252 	fi, fc, err := c.fileCache.WriteCloser(filenameContent)
  253 
  254 	return fi, fc, err
  255 }
  256 
  257 func (c *ResourceCache) set(key string, r any) {
  258 	c.Lock()
  259 	defer c.Unlock()
  260 	c.cache[key] = r
  261 }
  262 
  263 func (c *ResourceCache) DeletePartitions(partitions ...string) {
  264 	partitionsSet := map[string]bool{
  265 		// Always clear out the resources not matching any partition.
  266 		"other": true,
  267 	}
  268 	for _, p := range partitions {
  269 		partitionsSet[p] = true
  270 	}
  271 
  272 	if partitionsSet[CACHE_CLEAR_ALL] {
  273 		c.clear()
  274 		return
  275 	}
  276 
  277 	c.Lock()
  278 	defer c.Unlock()
  279 
  280 	for k := range c.cache {
  281 		clear := false
  282 		for p := range partitionsSet {
  283 			if strings.Contains(k, p) {
  284 				// There will be some false positive, but that's fine.
  285 				clear = true
  286 				break
  287 			}
  288 		}
  289 
  290 		if clear {
  291 			delete(c.cache, k)
  292 		}
  293 	}
  294 }
  295 
  296 func (c *ResourceCache) DeleteMatches(re *regexp.Regexp) {
  297 	c.Lock()
  298 	defer c.Unlock()
  299 
  300 	for k := range c.cache {
  301 		if re.MatchString(k) {
  302 			delete(c.cache, k)
  303 		}
  304 	}
  305 }