From cdc379d665d08600a0fd57f4672c4a48a79dc55e Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 4 Sep 2016 17:47:50 +0200 Subject: [PATCH] Add Cache --- src/restic/cache.go | 9 +++ src/restic/cache/cache.go | 118 +++++++++++++++++++++++++++++++++ src/restic/cache/cache_test.go | 75 +++++++++++++++++++++ src/restic/cache/testing.go | 18 +++++ 4 files changed, 220 insertions(+) create mode 100644 src/restic/cache.go create mode 100644 src/restic/cache/cache.go create mode 100644 src/restic/cache/cache_test.go create mode 100644 src/restic/cache/testing.go diff --git a/src/restic/cache.go b/src/restic/cache.go new file mode 100644 index 000000000..8e2ac7dca --- /dev/null +++ b/src/restic/cache.go @@ -0,0 +1,9 @@ +package restic + +// Cache stores blobs locally. +type Cache interface { + GetBlob(BlobHandle, []byte) (bool, error) + PutBlob(BlobHandle, []byte) error + DeleteBlob(BlobHandle) error + HasBlob(BlobHandle) bool +} diff --git a/src/restic/cache/cache.go b/src/restic/cache/cache.go new file mode 100644 index 000000000..355c757a6 --- /dev/null +++ b/src/restic/cache/cache.go @@ -0,0 +1,118 @@ +// Package cache implements a local cache for data. +package cache + +import ( + "io" + "io/ioutil" + "os" + "path/filepath" + "restic" + "restic/errors" +) + +// Cache is a local cache implementation. +type Cache struct { + dir string +} + +// make sure that Cache implement restic.Cache +var _ restic.Cache = &Cache{} + +// NewCache creates a new cache in the given directory. If it is the empty +// string, the cache directory for the current user is used instead. +func NewCache(dir string) restic.Cache { + return &Cache{dir: dir} +} + +func fn(dir string, h restic.BlobHandle) string { + return filepath.Join(dir, string(h.Type), h.ID.String()) +} + +// GetBlob returns a blob from the cache. If the blob is not in the cache, ok +// is set to false. +func (c *Cache) GetBlob(h restic.BlobHandle, buf []byte) (ok bool, err error) { + filename := fn(c.dir, h) + + fi, err := os.Stat(filename) + if os.IsNotExist(errors.Cause(err)) { + return false, nil + } + + if fi.Size() != int64(len(buf)) { + return false, errors.Errorf("wrong bufsize: %d != %d", fi.Size(), len(buf)) + } + + if err != nil { + return false, errors.Wrap(err, "Stat") + } + + var f *os.File + f, err = os.Open(filename) + if err != nil { + return false, errors.Wrap(err, "Open") + } + + defer func() { + e := f.Close() + if err == nil { + err = e + } + }() + + _, err = io.ReadFull(f, buf) + if err != nil { + return false, err + } + + return true, nil +} + +func createDirs(filename string) error { + dir := filepath.Dir(filename) + fi, err := os.Stat(dir) + if err != nil && os.IsNotExist(errors.Cause(err)) { + err = os.MkdirAll(dir, 0700) + return errors.Wrap(err, "mkdir cache dir") + } + + if err != nil { + return err + } + + if !fi.IsDir() { + return errors.Errorf("is not a directory: %v", dir) + } + + return nil +} + +// PutBlob saves a blob in the cache. +func (c *Cache) PutBlob(h restic.BlobHandle, buf []byte) error { + filename := fn(c.dir, h) + + if err := createDirs(filename); err != nil { + return err + } + + return ioutil.WriteFile(filename, buf, 0700) +} + +// DeleteBlob removes a blob from the cache. If it isn't included in the cache, +// a nil error is returned. +func (c *Cache) DeleteBlob(h restic.BlobHandle) error { + err := os.Remove(fn(c.dir, h)) + if err != nil && os.IsNotExist(errors.Cause(err)) { + err = nil + } + return err +} + +// HasBlob check whether the cache has a particular blob. +func (c *Cache) HasBlob(h restic.BlobHandle) bool { + _, err := os.Stat(fn(c.dir, h)) + if err != nil { + return false + } + + return true +} diff --git a/src/restic/cache/cache_test.go b/src/restic/cache/cache_test.go new file mode 100644 index 000000000..7753f36f7 --- /dev/null +++ b/src/restic/cache/cache_test.go @@ -0,0 +1,75 @@ +package cache + +import ( + "restic" + "restic/test" + "testing" +) + +func TestCache(t *testing.T) { + c, cleanup := TestNewCache(t) + defer cleanup() + + buf := test.Random(23, 2*1024*1024) + id := restic.Hash(buf) + + h := restic.BlobHandle{ID: id, Type: restic.DataBlob} + if c.HasBlob(h) { + t.Errorf("cache has blob before storing it") + } + + test.OK(t, c.PutBlob(h, buf)) + + if !c.HasBlob(h) { + t.Errorf("cache does not have blob after store") + } + + treeHandle := restic.BlobHandle{ID: id, Type: restic.TreeBlob} + if c.HasBlob(treeHandle) { + t.Errorf("cache has tree blob although only a data blob was stored") + } + + buf2 := make([]byte, len(buf)) + ok, err := c.GetBlob(h, buf2) + test.OK(t, err) + if !ok { + t.Errorf("could not get blob from cache") + } + + ok, err = c.GetBlob(treeHandle, buf2) + test.OK(t, err) + test.Assert(t, !ok, "got blob for tree that was never stored") + + err = c.DeleteBlob(treeHandle) + + test.OK(t, c.DeleteBlob(h)) + + if c.HasBlob(h) { + t.Errorf("cache still has blob after delete") + } +} + +func TestCacheBufsize(t *testing.T) { + c, cleanup := TestNewCache(t) + defer cleanup() + + h := restic.BlobHandle{ID: restic.NewRandomID(), Type: restic.TreeBlob} + buf := test.Random(5, 1000) + + test.OK(t, c.PutBlob(h, buf)) + + for i := len(buf) - 1; i <= len(buf)+1; i++ { + buf2 := make([]byte, i) + ok, err := c.GetBlob(h, buf2) + + if i == len(buf) { + test.OK(t, err) + test.Assert(t, ok, "unable to get blob for correct buf size") + test.Equals(t, buf, buf2) + continue + } + + test.Assert(t, !ok, "ok is true for wrong buffer size %v", i) + test.Assert(t, err != nil, "error is nil, although buffer size is wrong") + } +} diff --git a/src/restic/cache/testing.go b/src/restic/cache/testing.go new file mode 100644 index 000000000..84d544c1a --- /dev/null +++ b/src/restic/cache/testing.go @@ -0,0 +1,18 @@ +package cache + +import ( + "path/filepath" + "restic" + "restic/test" + "testing" +) + +// TestNewCache creates a cache usable for testing. +func TestNewCache(t testing.TB) (restic.Cache, func()) { + tempdir, cleanup := test.TempDir(t) + + cachedir := filepath.Join(tempdir, "cache") + c := NewCache(cachedir) + + return c, cleanup +}