restic/internal/backend/layout/layout.go

170 lines
4.3 KiB
Go
Raw Normal View History

package layout
2017-03-26 19:52:49 +00:00
import (
"context"
2017-04-02 15:25:22 +00:00
"fmt"
"os"
"path/filepath"
"regexp"
2017-07-23 12:21:03 +00:00
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs"
2017-07-24 15:42:25 +00:00
"github.com/restic/restic/internal/restic"
2017-03-26 19:52:49 +00:00
)
// Layout computes paths for file name storage.
type Layout interface {
Filename(restic.Handle) string
Dirname(restic.Handle) string
Basedir(restic.FileType) (dir string, subdirs bool)
2017-03-26 19:52:49 +00:00
Paths() []string
2017-06-07 19:59:41 +00:00
Name() string
2017-03-26 19:52:49 +00:00
}
2017-04-02 15:25:22 +00:00
// Filesystem is the abstraction of a file system used for a backend.
type Filesystem interface {
Join(...string) string
ReadDir(context.Context, string) ([]os.FileInfo, error)
2017-04-10 20:16:50 +00:00
IsNotExist(error) bool
2017-04-02 15:25:22 +00:00
}
// ensure statically that *LocalFilesystem implements Filesystem.
var _ Filesystem = &LocalFilesystem{}
// LocalFilesystem implements Filesystem in a local path.
type LocalFilesystem struct {
}
// ReadDir returns all entries of a directory.
func (l *LocalFilesystem) ReadDir(ctx context.Context, dir string) ([]os.FileInfo, error) {
2017-04-02 15:25:22 +00:00
f, err := fs.Open(dir)
if err != nil {
return nil, err
}
entries, err := f.Readdir(-1)
if err != nil {
2017-04-10 20:16:50 +00:00
return nil, errors.Wrap(err, "Readdir")
2017-04-02 15:25:22 +00:00
}
err = f.Close()
if err != nil {
2017-04-10 20:16:50 +00:00
return nil, errors.Wrap(err, "Close")
2017-04-02 15:25:22 +00:00
}
return entries, nil
}
// Join combines several path components to one.
func (l *LocalFilesystem) Join(paths ...string) string {
return filepath.Join(paths...)
}
2017-04-10 20:16:50 +00:00
// IsNotExist returns true for errors that are caused by not existing files.
func (l *LocalFilesystem) IsNotExist(err error) bool {
return os.IsNotExist(err)
}
2017-04-02 15:25:22 +00:00
var backendFilenameLength = len(restic.ID{}) * 2
var backendFilename = regexp.MustCompile(fmt.Sprintf("^[a-fA-F0-9]{%d}$", backendFilenameLength))
func hasBackendFile(ctx context.Context, fs Filesystem, dir string) (bool, error) {
entries, err := fs.ReadDir(ctx, dir)
if err != nil && fs.IsNotExist(err) {
2017-04-02 15:25:22 +00:00
return false, nil
}
if err != nil {
return false, errors.Wrap(err, "ReadDir")
}
for _, e := range entries {
if backendFilename.MatchString(e.Name()) {
return true, nil
}
}
return false, nil
}
// ErrLayoutDetectionFailed is returned by DetectLayout() when the layout
// cannot be detected automatically.
var ErrLayoutDetectionFailed = errors.New("auto-detecting the filesystem layout failed")
2017-04-02 15:25:22 +00:00
// DetectLayout tries to find out which layout is used in a local (or sftp)
2017-04-02 15:57:28 +00:00
// filesystem at the given path. If repo is nil, an instance of LocalFilesystem
// is used.
func DetectLayout(ctx context.Context, repo Filesystem, dir string) (Layout, error) {
2017-04-10 20:16:50 +00:00
debug.Log("detect layout at %v", dir)
2017-04-02 15:57:28 +00:00
if repo == nil {
repo = &LocalFilesystem{}
}
// key file in the "keys" dir (DefaultLayout)
foundKeysFile, err := hasBackendFile(ctx, repo, repo.Join(dir, defaultLayoutPaths[restic.KeyFile]))
2017-04-02 15:25:22 +00:00
if err != nil {
return nil, err
}
// key file in the "key" dir (S3LegacyLayout)
foundKeyFile, err := hasBackendFile(ctx, repo, repo.Join(dir, s3LayoutPaths[restic.KeyFile]))
2017-04-02 15:25:22 +00:00
if err != nil {
return nil, err
}
2017-05-15 21:36:23 +00:00
if foundKeysFile && !foundKeyFile {
2017-04-02 17:18:03 +00:00
debug.Log("found default layout at %v", dir)
return &DefaultLayout{
Path: dir,
Join: repo.Join,
}, nil
2017-04-02 15:25:22 +00:00
}
2017-05-15 21:36:23 +00:00
if foundKeyFile && !foundKeysFile {
2017-04-02 17:18:03 +00:00
debug.Log("found s3 layout at %v", dir)
2017-05-15 19:58:03 +00:00
return &S3LegacyLayout{
2017-04-02 17:18:03 +00:00
Path: dir,
Join: repo.Join,
}, nil
2017-04-02 15:25:22 +00:00
}
2017-04-10 20:16:50 +00:00
debug.Log("layout detection failed")
return nil, ErrLayoutDetectionFailed
2017-04-02 15:25:22 +00:00
}
2017-04-02 15:57:28 +00:00
// ParseLayout parses the config string and returns a Layout. When layout is
// the empty string, DetectLayout is used. If that fails, defaultLayout is used.
func ParseLayout(ctx context.Context, repo Filesystem, layout, defaultLayout, path string) (l Layout, err error) {
2017-04-02 17:18:03 +00:00
debug.Log("parse layout string %q for backend at %v", layout, path)
2017-04-02 15:57:28 +00:00
switch layout {
case "default":
l = &DefaultLayout{
Path: path,
Join: repo.Join,
}
2017-05-15 19:58:03 +00:00
case "s3legacy":
l = &S3LegacyLayout{
2017-04-02 15:57:28 +00:00
Path: path,
Join: repo.Join,
}
case "":
l, err = DetectLayout(ctx, repo, path)
// use the default layout if auto detection failed
if errors.Is(err, ErrLayoutDetectionFailed) && defaultLayout != "" {
2017-05-15 21:36:23 +00:00
debug.Log("error: %v, use default layout %v", err, defaultLayout)
return ParseLayout(ctx, repo, defaultLayout, "", path)
}
2017-04-10 20:16:50 +00:00
if err != nil {
return nil, err
}
debug.Log("layout detected: %v", l)
2017-04-02 15:57:28 +00:00
default:
return nil, errors.Errorf("unknown backend layout string %q, may be one of: default, s3legacy", layout)
2017-04-02 15:57:28 +00:00
}
return l, nil
}