forked from TrueCloudLab/restic
97a307df1a
A file is always cached whole. Thus, any out of bounds access will also fail when directed at the backend. To handle case in which the cached file is broken, then caller must call Cache.Forget(h) for the file in question.
227 lines
5.3 KiB
Go
227 lines
5.3 KiB
Go
package cache
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/restic/restic/internal/backend"
|
|
"github.com/restic/restic/internal/backend/util"
|
|
"github.com/restic/restic/internal/crypto"
|
|
"github.com/restic/restic/internal/debug"
|
|
"github.com/restic/restic/internal/fs"
|
|
"github.com/restic/restic/internal/restic"
|
|
)
|
|
|
|
func (c *Cache) filename(h backend.Handle) string {
|
|
if len(h.Name) < 2 {
|
|
panic("Name is empty or too short")
|
|
}
|
|
subdir := h.Name[:2]
|
|
return filepath.Join(c.path, cacheLayoutPaths[h.Type], subdir, h.Name)
|
|
}
|
|
|
|
func (c *Cache) canBeCached(t backend.FileType) bool {
|
|
if c == nil {
|
|
return false
|
|
}
|
|
|
|
_, ok := cacheLayoutPaths[t]
|
|
return ok
|
|
}
|
|
|
|
// load returns a reader that yields the contents of the file with the
|
|
// given handle. rd must be closed after use. If an error is returned, the
|
|
// ReadCloser is nil. The bool return value indicates whether the requested
|
|
// file exists in the cache. It can be true even when no reader is returned
|
|
// because length or offset are out of bounds
|
|
func (c *Cache) load(h backend.Handle, length int, offset int64) (io.ReadCloser, bool, error) {
|
|
debug.Log("Load(%v, %v, %v) from cache", h, length, offset)
|
|
if !c.canBeCached(h.Type) {
|
|
return nil, false, errors.New("cannot be cached")
|
|
}
|
|
|
|
f, err := fs.Open(c.filename(h))
|
|
if err != nil {
|
|
return nil, false, errors.WithStack(err)
|
|
}
|
|
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
_ = f.Close()
|
|
return nil, true, errors.WithStack(err)
|
|
}
|
|
|
|
size := fi.Size()
|
|
if size <= int64(crypto.CiphertextLength(0)) {
|
|
_ = f.Close()
|
|
return nil, true, errors.Errorf("cached file %v is truncated", h)
|
|
}
|
|
|
|
if size < offset+int64(length) {
|
|
_ = f.Close()
|
|
return nil, true, errors.Errorf("cached file %v is too short", h)
|
|
}
|
|
|
|
if offset > 0 {
|
|
if _, err = f.Seek(offset, io.SeekStart); err != nil {
|
|
_ = f.Close()
|
|
return nil, true, err
|
|
}
|
|
}
|
|
|
|
if length <= 0 {
|
|
return f, true, nil
|
|
}
|
|
return util.LimitReadCloser(f, int64(length)), true, nil
|
|
}
|
|
|
|
// save saves a file in the cache.
|
|
func (c *Cache) save(h backend.Handle, rd io.Reader) error {
|
|
debug.Log("Save to cache: %v", h)
|
|
if rd == nil {
|
|
return errors.New("Save() called with nil reader")
|
|
}
|
|
if !c.canBeCached(h.Type) {
|
|
return errors.New("cannot be cached")
|
|
}
|
|
|
|
finalname := c.filename(h)
|
|
dir := filepath.Dir(finalname)
|
|
err := fs.Mkdir(dir, 0700)
|
|
if err != nil && !errors.Is(err, os.ErrExist) {
|
|
return err
|
|
}
|
|
|
|
// First save to a temporary location. This allows multiple concurrent
|
|
// restics to use a single cache dir.
|
|
f, err := os.CreateTemp(dir, "tmp-")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n, err := io.Copy(f, rd)
|
|
if err != nil {
|
|
_ = f.Close()
|
|
_ = fs.Remove(f.Name())
|
|
return errors.Wrap(err, "Copy")
|
|
}
|
|
|
|
if n <= int64(crypto.CiphertextLength(0)) {
|
|
_ = f.Close()
|
|
_ = fs.Remove(f.Name())
|
|
debug.Log("trying to cache truncated file %v, removing", h)
|
|
return nil
|
|
}
|
|
|
|
// Close, then rename. Windows doesn't like the reverse order.
|
|
if err = f.Close(); err != nil {
|
|
_ = fs.Remove(f.Name())
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
err = fs.Rename(f.Name(), finalname)
|
|
if err != nil {
|
|
_ = fs.Remove(f.Name())
|
|
}
|
|
if runtime.GOOS == "windows" && errors.Is(err, os.ErrPermission) {
|
|
// On Windows, renaming over an existing file is ok
|
|
// (os.Rename is MoveFileExW with MOVEFILE_REPLACE_EXISTING
|
|
// since Go 1.5), but not when someone else has the file open.
|
|
//
|
|
// When we get Access denied, we assume that's the case
|
|
// and the other process has written the desired contents to f.
|
|
err = nil
|
|
}
|
|
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
func (c *Cache) Forget(h backend.Handle) error {
|
|
return c.remove(h)
|
|
}
|
|
|
|
// remove deletes a file. When the file is not cached, no error is returned.
|
|
func (c *Cache) remove(h backend.Handle) error {
|
|
if !c.canBeCached(h.Type) {
|
|
return nil
|
|
}
|
|
|
|
err := fs.Remove(c.filename(h))
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
err = nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Clear removes all files of type t from the cache that are not contained in
|
|
// the set valid.
|
|
func (c *Cache) Clear(t restic.FileType, valid restic.IDSet) error {
|
|
debug.Log("Clearing cache for %v: %v valid files", t, len(valid))
|
|
if !c.canBeCached(t) {
|
|
return nil
|
|
}
|
|
|
|
list, err := c.list(t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for id := range list {
|
|
if valid.Has(id) {
|
|
continue
|
|
}
|
|
|
|
// ignore ErrNotExist to gracefully handle multiple processes running Clear() concurrently
|
|
if err = fs.Remove(c.filename(backend.Handle{Type: t, Name: id.String()})); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isFile(fi os.FileInfo) bool {
|
|
return fi.Mode()&(os.ModeType|os.ModeCharDevice) == 0
|
|
}
|
|
|
|
// list returns a list of all files of type T in the cache.
|
|
func (c *Cache) list(t restic.FileType) (restic.IDSet, error) {
|
|
if !c.canBeCached(t) {
|
|
return nil, errors.New("cannot be cached")
|
|
}
|
|
|
|
list := restic.NewIDSet()
|
|
dir := filepath.Join(c.path, cacheLayoutPaths[t])
|
|
err := filepath.Walk(dir, func(name string, fi os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return errors.Wrap(err, "Walk")
|
|
}
|
|
|
|
if !isFile(fi) {
|
|
return nil
|
|
}
|
|
|
|
id, err := restic.ParseID(filepath.Base(name))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
list.Insert(id)
|
|
return nil
|
|
})
|
|
|
|
return list, err
|
|
}
|
|
|
|
// Has returns true if the file is cached.
|
|
func (c *Cache) Has(h backend.Handle) bool {
|
|
if !c.canBeCached(h.Type) {
|
|
return false
|
|
}
|
|
|
|
_, err := fs.Stat(c.filename(h))
|
|
return err == nil
|
|
}
|