github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/cache/filecache/filecache.go (about)

     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  	"bytes"
    18  	"errors"
    19  	"io"
    20  	"os"
    21  	"path/filepath"
    22  	"strings"
    23  	"sync"
    24  	"time"
    25  
    26  	"github.com/gohugoio/hugo/common/hugio"
    27  
    28  	"github.com/gohugoio/hugo/helpers"
    29  
    30  	"github.com/BurntSushi/locker"
    31  	"github.com/spf13/afero"
    32  )
    33  
    34  // ErrFatal can be used to signal an unrecoverable error.
    35  var ErrFatal = errors.New("fatal filecache error")
    36  
    37  const (
    38  	FilecacheRootDirname = "filecache"
    39  )
    40  
    41  // Cache caches a set of files in a directory. This is usually a file on
    42  // disk, but since this is backed by an Afero file system, it can be anything.
    43  type Cache struct {
    44  	Fs afero.Fs
    45  
    46  	// Max age for items in this cache. Negative duration means forever,
    47  	// 0 is effectively turning this cache off.
    48  	maxAge time.Duration
    49  
    50  	// When set, we just remove this entire root directory on expiration.
    51  	pruneAllRootDir string
    52  
    53  	nlocker *lockTracker
    54  
    55  	initOnce sync.Once
    56  	initErr  error
    57  }
    58  
    59  type lockTracker struct {
    60  	seenMu sync.RWMutex
    61  	seen   map[string]struct{}
    62  
    63  	*locker.Locker
    64  }
    65  
    66  // Lock tracks the ids in use. We use this information to do garbage collection
    67  // after a Hugo build.
    68  func (l *lockTracker) Lock(id string) {
    69  	l.seenMu.RLock()
    70  	if _, seen := l.seen[id]; !seen {
    71  		l.seenMu.RUnlock()
    72  		l.seenMu.Lock()
    73  		l.seen[id] = struct{}{}
    74  		l.seenMu.Unlock()
    75  	} else {
    76  		l.seenMu.RUnlock()
    77  	}
    78  
    79  	l.Locker.Lock(id)
    80  }
    81  
    82  // ItemInfo contains info about a cached file.
    83  type ItemInfo struct {
    84  	// This is the file's name relative to the cache's filesystem.
    85  	Name string
    86  }
    87  
    88  // NewCache creates a new file cache with the given filesystem and max age.
    89  func NewCache(fs afero.Fs, maxAge time.Duration, pruneAllRootDir string) *Cache {
    90  	return &Cache{
    91  		Fs:              fs,
    92  		nlocker:         &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
    93  		maxAge:          maxAge,
    94  		pruneAllRootDir: pruneAllRootDir,
    95  	}
    96  }
    97  
    98  // lockedFile is a file with a lock that is released on Close.
    99  type lockedFile struct {
   100  	afero.File
   101  	unlock func()
   102  }
   103  
   104  func (l *lockedFile) Close() error {
   105  	defer l.unlock()
   106  	return l.File.Close()
   107  }
   108  
   109  func (c *Cache) init() error {
   110  	c.initOnce.Do(func() {
   111  		// Create the base dir if it does not exist.
   112  		if err := c.Fs.MkdirAll("", 0777); err != nil && !os.IsExist(err) {
   113  			c.initErr = err
   114  		}
   115  	})
   116  	return c.initErr
   117  }
   118  
   119  // WriteCloser returns a transactional writer into the cache.
   120  // It's important that it's closed when done.
   121  func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) {
   122  	if err := c.init(); err != nil {
   123  		return ItemInfo{}, nil, err
   124  	}
   125  
   126  	id = cleanID(id)
   127  	c.nlocker.Lock(id)
   128  
   129  	info := ItemInfo{Name: id}
   130  
   131  	f, err := helpers.OpenFileForWriting(c.Fs, id)
   132  	if err != nil {
   133  		c.nlocker.Unlock(id)
   134  		return info, nil, err
   135  	}
   136  
   137  	return info, &lockedFile{
   138  		File:   f,
   139  		unlock: func() { c.nlocker.Unlock(id) },
   140  	}, nil
   141  }
   142  
   143  // ReadOrCreate tries to lookup the file in cache.
   144  // If found, it is passed to read and then closed.
   145  // If not found a new file is created and passed to create, which should close
   146  // it when done.
   147  func (c *Cache) ReadOrCreate(id string,
   148  	read func(info ItemInfo, r io.ReadSeeker) error,
   149  	create func(info ItemInfo, w io.WriteCloser) error) (info ItemInfo, err error) {
   150  	if err := c.init(); err != nil {
   151  		return ItemInfo{}, err
   152  	}
   153  
   154  	id = cleanID(id)
   155  
   156  	c.nlocker.Lock(id)
   157  	defer c.nlocker.Unlock(id)
   158  
   159  	info = ItemInfo{Name: id}
   160  
   161  	if r := c.getOrRemove(id); r != nil {
   162  		err = read(info, r)
   163  		defer r.Close()
   164  		if err == nil || err == ErrFatal {
   165  			// See https://github.com/gohugoio/hugo/issues/6401
   166  			// To recover from file corruption we handle read errors
   167  			// as the cache item was not found.
   168  			// Any file permission issue will also fail in the next step.
   169  			return
   170  		}
   171  	}
   172  
   173  	f, err := helpers.OpenFileForWriting(c.Fs, id)
   174  	if err != nil {
   175  		return
   176  	}
   177  
   178  	err = create(info, f)
   179  
   180  	return
   181  }
   182  
   183  // GetOrCreate tries to get the file with the given id from cache. If not found or expired, create will
   184  // be invoked and the result cached.
   185  // This method is protected by a named lock using the given id as identifier.
   186  func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (ItemInfo, io.ReadCloser, error) {
   187  	if err := c.init(); err != nil {
   188  		return ItemInfo{}, nil, err
   189  	}
   190  	id = cleanID(id)
   191  
   192  	c.nlocker.Lock(id)
   193  	defer c.nlocker.Unlock(id)
   194  
   195  	info := ItemInfo{Name: id}
   196  
   197  	if r := c.getOrRemove(id); r != nil {
   198  		return info, r, nil
   199  	}
   200  
   201  	var (
   202  		r   io.ReadCloser
   203  		err error
   204  	)
   205  
   206  	r, err = create()
   207  	if err != nil {
   208  		return info, nil, err
   209  	}
   210  
   211  	if c.maxAge == 0 {
   212  		// No caching.
   213  		return info, hugio.ToReadCloser(r), nil
   214  	}
   215  
   216  	var buff bytes.Buffer
   217  	return info,
   218  		hugio.ToReadCloser(&buff),
   219  		afero.WriteReader(c.Fs, id, io.TeeReader(r, &buff))
   220  }
   221  
   222  // GetOrCreateBytes is the same as GetOrCreate, but produces a byte slice.
   223  func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (ItemInfo, []byte, error) {
   224  	if err := c.init(); err != nil {
   225  		return ItemInfo{}, nil, err
   226  	}
   227  	id = cleanID(id)
   228  
   229  	c.nlocker.Lock(id)
   230  	defer c.nlocker.Unlock(id)
   231  
   232  	info := ItemInfo{Name: id}
   233  
   234  	if r := c.getOrRemove(id); r != nil {
   235  		defer r.Close()
   236  		b, err := io.ReadAll(r)
   237  		return info, b, err
   238  	}
   239  
   240  	var (
   241  		b   []byte
   242  		err error
   243  	)
   244  
   245  	b, err = create()
   246  	if err != nil {
   247  		return info, nil, err
   248  	}
   249  
   250  	if c.maxAge == 0 {
   251  		return info, b, nil
   252  	}
   253  
   254  	if err := afero.WriteReader(c.Fs, id, bytes.NewReader(b)); err != nil {
   255  		return info, nil, err
   256  	}
   257  	return info, b, nil
   258  }
   259  
   260  // GetBytes gets the file content with the given id from the cache, nil if none found.
   261  func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
   262  	if err := c.init(); err != nil {
   263  		return ItemInfo{}, nil, err
   264  	}
   265  	id = cleanID(id)
   266  
   267  	c.nlocker.Lock(id)
   268  	defer c.nlocker.Unlock(id)
   269  
   270  	info := ItemInfo{Name: id}
   271  
   272  	if r := c.getOrRemove(id); r != nil {
   273  		defer r.Close()
   274  		b, err := io.ReadAll(r)
   275  		return info, b, err
   276  	}
   277  
   278  	return info, nil, nil
   279  }
   280  
   281  // Get gets the file with the given id from the cache, nil if none found.
   282  func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
   283  	if err := c.init(); err != nil {
   284  		return ItemInfo{}, nil, err
   285  	}
   286  	id = cleanID(id)
   287  
   288  	c.nlocker.Lock(id)
   289  	defer c.nlocker.Unlock(id)
   290  
   291  	info := ItemInfo{Name: id}
   292  
   293  	r := c.getOrRemove(id)
   294  
   295  	return info, r, nil
   296  }
   297  
   298  // getOrRemove gets the file with the given id. If it's expired, it will
   299  // be removed.
   300  func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
   301  	if c.maxAge == 0 {
   302  		// No caching.
   303  		return nil
   304  	}
   305  
   306  	if c.maxAge > 0 {
   307  		fi, err := c.Fs.Stat(id)
   308  		if err != nil {
   309  			return nil
   310  		}
   311  
   312  		if c.isExpired(fi.ModTime()) {
   313  			c.Fs.Remove(id)
   314  			return nil
   315  		}
   316  	}
   317  
   318  	f, err := c.Fs.Open(id)
   319  	if err != nil {
   320  		return nil
   321  	}
   322  
   323  	return f
   324  }
   325  
   326  func (c *Cache) isExpired(modTime time.Time) bool {
   327  	if c.maxAge < 0 {
   328  		return false
   329  	}
   330  
   331  	// Note the use of time.Since here.
   332  	// We cannot use Hugo's global Clock for this.
   333  	return c.maxAge == 0 || time.Since(modTime) > c.maxAge
   334  }
   335  
   336  // For testing
   337  func (c *Cache) GetString(id string) string {
   338  	id = cleanID(id)
   339  
   340  	c.nlocker.Lock(id)
   341  	defer c.nlocker.Unlock(id)
   342  
   343  	f, err := c.Fs.Open(id)
   344  	if err != nil {
   345  		return ""
   346  	}
   347  	defer f.Close()
   348  
   349  	b, _ := io.ReadAll(f)
   350  	return string(b)
   351  }
   352  
   353  // Caches is a named set of caches.
   354  type Caches map[string]*Cache
   355  
   356  // Get gets a named cache, nil if none found.
   357  func (f Caches) Get(name string) *Cache {
   358  	return f[strings.ToLower(name)]
   359  }
   360  
   361  // NewCaches creates a new set of file caches from the given
   362  // configuration.
   363  func NewCaches(p *helpers.PathSpec) (Caches, error) {
   364  	dcfg := p.Cfg.GetConfigSection("caches").(Configs)
   365  	fs := p.Fs.Source
   366  
   367  	m := make(Caches)
   368  	for k, v := range dcfg {
   369  		var cfs afero.Fs
   370  
   371  		if v.IsResourceDir {
   372  			cfs = p.BaseFs.ResourcesCache
   373  		} else {
   374  			cfs = fs
   375  		}
   376  
   377  		if cfs == nil {
   378  			panic("nil fs")
   379  		}
   380  
   381  		baseDir := v.DirCompiled
   382  
   383  		bfs := afero.NewBasePathFs(cfs, baseDir)
   384  
   385  		var pruneAllRootDir string
   386  		if k == CacheKeyModules {
   387  			pruneAllRootDir = "pkg"
   388  		}
   389  
   390  		m[k] = NewCache(bfs, v.MaxAge, pruneAllRootDir)
   391  	}
   392  
   393  	return m, nil
   394  }
   395  
   396  func cleanID(name string) string {
   397  	return strings.TrimPrefix(filepath.Clean(name), helpers.FilePathSeparator)
   398  }