cache/filecache: Add a :project placeholder
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 14 Nov 2018 16:18:32 +0000 (17:18 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 14 Nov 2018 22:14:51 +0000 (23:14 +0100)
This allows for "cache per Hugo project", making `hugo --gc` work as expected, even if you have several Hugo projects running on the same PC.

See #5439

cache/filecache/filecache.go
cache/filecache/filecache_config.go
cache/filecache/filecache_config_test.go
docs/content/en/getting-started/configuration.md
hugolib/site_test.go

index e9f72cb920e3ead25ee57429e6922389979c39ea..9f50ad6b66f129724fa8e42ae2b3b4f3895e78db 100644 (file)
@@ -272,7 +272,10 @@ func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
 }
 
 func (c *Cache) isExpired(modTime time.Time) bool {
-       return c.maxAge >= 0 && time.Now().Sub(modTime) > c.maxAge
+       if c.maxAge < 0 {
+               return false
+       }
+       return c.maxAge == 0 || time.Now().Sub(modTime) > c.maxAge
 }
 
 // For testing
index f0dd7295a86e2ceca4862413071a02ef2c2ff764..bb2cc36e72db0b5004da05f9df9d59e938f2ace0 100644 (file)
@@ -35,7 +35,7 @@ const (
 
 var defaultCacheConfig = cacheConfig{
        MaxAge: -1, // Never expire
-       Dir:    ":cacheDir",
+       Dir:    ":cacheDir/:project",
 }
 
 const (
@@ -139,26 +139,33 @@ func decodeConfig(p *paths.Paths) (cachesConfig, error) {
        disabled := cfg.GetBool("ignoreCache")
 
        for k, v := range c {
-               v.Dir = filepath.Clean(v.Dir)
-               dir := filepath.ToSlash(v.Dir)
+               dir := filepath.ToSlash(filepath.Clean(v.Dir))
+               hadSlash := strings.HasPrefix(dir, "/")
                parts := strings.Split(dir, "/")
-               first := parts[0]
 
-               if strings.HasPrefix(first, ":") {
-                       resolved, err := resolveDirPlaceholder(p, first)
-                       if err != nil {
-                               return c, err
+               for i, part := range parts {
+                       if strings.HasPrefix(part, ":") {
+                               resolved, err := resolveDirPlaceholder(p, part)
+                               if err != nil {
+                                       return c, err
+                               }
+                               parts[i] = resolved
                        }
-                       resolved = filepath.ToSlash(resolved)
+               }
 
-                       v.Dir = filepath.FromSlash(path.Join((append([]string{resolved}, parts[1:]...))...))
+               dir = path.Join(parts...)
+               if hadSlash {
+                       dir = "/" + dir
+               }
+               v.Dir = filepath.Clean(filepath.FromSlash(dir))
 
-               } else if isOsFs && !path.IsAbs(dir) {
-                       return c, errors.Errorf("%q must either start with a placeholder (e.g. :cacheDir, :resourceDir) or be absolute", v.Dir)
+               if isOsFs && !filepath.IsAbs(v.Dir) {
+                       return c, errors.Errorf("%q must resolve to an absolute directory", v.Dir)
                }
 
-               if len(v.Dir) < 5 {
-                       return c, errors.Errorf("%q is not a valid cache dir", v.Dir)
+               // Avoid cache in root, e.g. / (Unix) or c:\ (Windows)
+               if len(strings.TrimPrefix(v.Dir, filepath.VolumeName(v.Dir))) == 1 {
+                       return c, errors.Errorf("%q is a root folder and not allowed as cache dir", v.Dir)
                }
 
                if disabled {
@@ -178,6 +185,8 @@ func resolveDirPlaceholder(p *paths.Paths, placeholder string) (string, error) {
                return p.AbsResourcesDir, nil
        case ":cachedir":
                return helpers.GetCacheDir(p.Fs.Source, p.Cfg)
+       case ":project":
+               return filepath.Base(p.WorkingDir), nil
        }
 
        return "", errors.Errorf("%q is not a valid placeholder (valid values are :cacheDir or :resourceDir)", placeholder)
index abba6c25f51293701618aa0b7c60b3e68c468b65..51126f08082980593abe23b6fa33a7eb97250a46 100644 (file)
@@ -16,6 +16,7 @@ package filecache
 import (
        "path/filepath"
        "runtime"
+       "strings"
        "testing"
        "time"
 
@@ -107,6 +108,8 @@ dir = "/path/to/c3"
 func TestDecodeConfigDefault(t *testing.T) {
        assert := require.New(t)
        cfg := viper.New()
+       cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject"))
+
        if runtime.GOOS == "windows" {
                cfg.Set("resourceDir", "c:\\cache\\resources")
                cfg.Set("cacheDir", "c:\\cache\\thecache")
@@ -130,5 +133,34 @@ func TestDecodeConfigDefault(t *testing.T) {
                assert.Equal("c:\\cache\\resources\\_gen", decoded[cacheKeyImages].Dir)
        } else {
                assert.Equal("/cache/resources/_gen", decoded[cacheKeyImages].Dir)
+               assert.Equal("/cache/thecache/hugoproject", decoded[cacheKeyGetJSON].Dir)
+       }
+}
+
+func TestDecodeConfigInvalidDir(t *testing.T) {
+       t.Parallel()
+
+       assert := require.New(t)
+
+       configStr := `
+resourceDir = "myresources"
+[caches]
+[caches.getJSON]
+maxAge = "10m"
+dir = "/"
+
+`
+       if runtime.GOOS == "windows" {
+               configStr = strings.Replace(configStr, "/", "c:\\\\", 1)
        }
+
+       cfg, err := config.FromConfigString(configStr, "toml")
+       assert.NoError(err)
+       fs := hugofs.NewMem(cfg)
+       p, err := paths.New(fs, cfg)
+       assert.NoError(err)
+
+       _, err = decodeConfig(p)
+       assert.Error(err)
+
 }
index cb2eed4cde051799f7ae68a337fe6ba9af9f58ad..f67b9370bcb3d5ff1e583ae8372d4b8898d634a3 100644 (file)
@@ -413,10 +413,10 @@ Since Hugo 0.52 you can configure more than just the `cacheDir`. This is the def
 ```toml
 [caches]
 [caches.getjson]
-dir = ":cacheDir"
+dir = ":cacheDir/:project"
 maxAge = -1
 [caches.getcsv]
-dir = ":cacheDir"
+dir = ":cacheDir/:project"
 maxAge = -1
 [caches.images]
 dir = ":resourceDir/_gen"
@@ -434,6 +434,9 @@ You can override any of these cache setting in your own `config.toml`.
 :cacheDir
 : This is the value of the `cacheDir` config option if set (can also be set via OS env variable `HUGO_CACHEDIR`). It will fall back to `/opt/build/cache/hugo_cache/` on Netlify, or a `hugo_cache` directory below the OS temp dir for the others. This means that if you run your builds on Netlify, all caches configured with `:cacheDir` will be saved and restored on the next build. For other CI vendors, please read their documentation. For an CircleCI example, see [this configuration](https://github.com/bep/hugo-sass-test/blob/6c3960a8f4b90e8938228688bc49bdcdd6b2d99e/.circleci/config.yml).
 
+:project
+
+The base directory name of the current Hugo project. This means that, in its default setting, every project will have separated file caches, which means that when you do `hugo --gc` you will not touch files related to other Hugo projects running on the same PC.
 
 :resourceDir
 : This is the value of the `resourceDir` config option.
index 0fd3a397a3ce640562d54b2141960fa2f5b1bc98..4f8d43122ff5a5231f53960bca98943527ef5704 100644 (file)
@@ -15,7 +15,6 @@ package hugolib
 
 import (
        "fmt"
-       "os"
        "path/filepath"
        "strings"
        "testing"
@@ -25,7 +24,6 @@ import (
        "github.com/gohugoio/hugo/helpers"
 
        "github.com/gohugoio/hugo/deps"
-       "github.com/gohugoio/hugo/hugofs"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
 )
@@ -351,15 +349,6 @@ func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) {
 
 }
 
-func TestNewSiteDefaultLang(t *testing.T) {
-       t.Parallel()
-       defer os.Remove("resources")
-       s, err := NewSiteDefaultLang()
-       require.NoError(t, err)
-       require.Equal(t, hugofs.Os, s.Fs.Source)
-       require.Equal(t, hugofs.Os, s.Fs.Destination)
-}
-
 // Issue #3355
 func TestShouldNotWriteZeroLengthFilesToDestination(t *testing.T) {
        cfg, fs := newTestCfg()