cache/filecache: Recover from file corruption
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 21 Oct 2019 07:37:46 +0000 (09:37 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 21 Oct 2019 07:51:51 +0000 (09:51 +0200)
Fixes #6401

cache/filecache/filecache.go
cache/filecache/filecache_test.go

index bc0573d52c78a7550c179622b949cab7c2e8d3b3..3628300fb651d7a4e7147b35bb1d112bb6be1abe 100644 (file)
@@ -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)
index 6d3ea6289f8501e419f2478efc57a1bd1cb4ea39..a4bf45fe020b04b39834461938ba8ab3a314065e 100644 (file)
@@ -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"))