hugo

Unnamed repository; edit this file 'description' to name the repository.

git clone git://git.shimmy1996.com/hugo.git
commit 180195aa342777fece1b29a08ec89456d7996c61
parent 4b286b9d2722909d0682e50eeecdfe16c1f47fd8
Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Date:   Mon, 21 Oct 2019 09:37:46 +0200

cache/filecache: Recover from file corruption

Fixes #6401

Diffstat:
Mcache/filecache/filecache.go | 12+++++++++++-
Mcache/filecache/filecache_test.go | 50++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 61 insertions(+), 1 deletion(-)
diff --git a/cache/filecache/filecache.go b/cache/filecache/filecache.go
@@ -15,6 +15,7 @@ package filecache
 
 import (
 	"bytes"
+	"errors"
 	"io"
 	"io/ioutil"
 	"os"
@@ -31,6 +32,9 @@ import (
 	"github.com/spf13/afero"
 )
 
+// ErrFatal can be used to signal an unrecoverable error.
+var ErrFatal = errors.New("fatal filecache error")
+
 const (
 	filecacheRootDirname = "filecache"
 )
@@ -137,7 +141,13 @@ func (c *Cache) ReadOrCreate(id string,
 	if r := c.getOrRemove(id); r != nil {
 		err = read(info, r)
 		defer r.Close()
-		return
+		if err == nil || err == ErrFatal {
+			// See https://github.com/gohugoio/hugo/issues/6401
+			// To recover from file corruption we handle read errors
+			// as the cache item was not found.
+			// Any file permission issue will also fail in the next step.
+			return
+		}
 	}
 
 	f, err := helpers.OpenFileForWriting(c.Fs, id)
diff --git a/cache/filecache/filecache_test.go b/cache/filecache/filecache_test.go
@@ -14,6 +14,7 @@
 package filecache
 
 import (
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -243,6 +244,55 @@ dir = "/cache/c"
 	wg.Wait()
 }
 
+func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
+	t.Parallel()
+	c := qt.New(t)
+
+	var result string
+
+	rf := func(failLevel int) func(info ItemInfo, r io.Reader) error {
+
+		return func(info ItemInfo, r io.Reader) error {
+			if failLevel > 0 {
+				if failLevel > 1 {
+					return ErrFatal
+				}
+				return errors.New("fail")
+			}
+
+			b, _ := ioutil.ReadAll(r)
+			result = string(b)
+
+			return nil
+		}
+	}
+
+	bf := func(s string) func(info ItemInfo, w io.WriteCloser) error {
+		return func(info ItemInfo, w io.WriteCloser) error {
+			defer w.Close()
+			result = s
+			_, err := w.Write([]byte(s))
+			return err
+		}
+	}
+
+	cache := NewCache(afero.NewMemMapFs(), 100*time.Hour, "")
+
+	const id = "a32"
+
+	_, err := cache.ReadOrCreate(id, rf(0), bf("v1"))
+	c.Assert(err, qt.IsNil)
+	c.Assert(result, qt.Equals, "v1")
+	_, err = cache.ReadOrCreate(id, rf(0), bf("v2"))
+	c.Assert(err, qt.IsNil)
+	c.Assert(result, qt.Equals, "v1")
+	_, err = cache.ReadOrCreate(id, rf(1), bf("v3"))
+	c.Assert(err, qt.IsNil)
+	c.Assert(result, qt.Equals, "v3")
+	_, err = cache.ReadOrCreate(id, rf(2), bf("v3"))
+	c.Assert(err, qt.Equals, ErrFatal)
+}
+
 func TestCleanID(t *testing.T) {
 	c := qt.New(t)
 	c.Assert(cleanID(filepath.FromSlash("/a/b//c.txt")), qt.Equals, filepath.FromSlash("a/b/c.txt"))