github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/resources/resource_cache.go (about)

     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]interface{}
    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]interface{}),
   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]interface{})
   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) (interface{}, 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() (interface{}, 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() (interface{}, 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() (interface{}, error)) (interface{}, 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 interface{}) {
   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  }