From 180195aa342777fece1b29a08ec89456d7996c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Mon, 21 Oct 2019 09:37:46 +0200 Subject: [PATCH] cache/filecache: Recover from file corruption Fixes #6401 --- cache/filecache/filecache.go | 12 +++++++- cache/filecache/filecache_test.go | 50 +++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/cache/filecache/filecache.go b/cache/filecache/filecache.go index bc0573d52..3628300fb 100644 --- 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 index 6d3ea6289..a4bf45fe0 100644 --- 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"))